Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87507edf59 | ||
|
|
c54144f2e4 | ||
|
|
2b06b00d35 | ||
|
|
28d931dfff | ||
|
|
e2f7bea299 | ||
|
|
d5e834bc78 | ||
|
|
687d345882 | ||
|
|
3e565e8d03 | ||
|
|
358a520bc5 | ||
|
|
9dffe83f32 | ||
|
|
d64e899993 | ||
|
|
f37303b22d | ||
|
|
d1b0c55b53 | ||
|
|
8fc6f15402 | ||
|
|
6afe32f63f | ||
|
|
76c768099d | ||
|
|
30e5721e84 |
+12
@@ -7,3 +7,15 @@ dist/
|
||||
*.db-wal
|
||||
.DS_Store
|
||||
bun.lockb
|
||||
|
||||
.publish/
|
||||
|
||||
# Environment files may contain secrets. Commit only templates.
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.*.example
|
||||
|
||||
# Local focused typecheck helpers must never enter the repository.
|
||||
focus-shims.d.ts
|
||||
tsconfig.focus.json
|
||||
|
||||
@@ -15,3 +15,9 @@ bun.lockb
|
||||
editors/vscode/src/compiler.cjs
|
||||
*.svg
|
||||
**/.vscodeignore
|
||||
|
||||
# Local focused typecheck helpers (never part of a release)
|
||||
focus-shims.d.ts
|
||||
**/focus-shims.d.ts
|
||||
tsconfig.focus.json
|
||||
**/tsconfig.focus.json
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ai",
|
||||
"version": "0.3.6",
|
||||
"version": "0.7.0",
|
||||
"type": "module",
|
||||
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/authz",
|
||||
"version": "0.3.6",
|
||||
"version": "0.7.0",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/authz — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
+13
-13
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/cli",
|
||||
"version": "0.3.6",
|
||||
"version": "0.7.0",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/cli — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -28,18 +28,18 @@
|
||||
"wrnexus": "./dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "^0.3.6",
|
||||
"@wrnexus/router": "^0.3.6",
|
||||
"@wrnexus/csr": "^0.3.6",
|
||||
"@wrnexus/compiler": "^0.3.6",
|
||||
"@wrnexus/styles": "^0.3.6",
|
||||
"@wrnexus/dev-server": "^0.3.6",
|
||||
"@wrnexus/ui": "^0.3.6",
|
||||
"@wrnexus/validation": "^0.3.6",
|
||||
"@wrnexus/i18n": "^0.3.6",
|
||||
"@wrnexus/db": "^0.3.6",
|
||||
"@wrnexus/plugin": "^0.3.6",
|
||||
"@wrnexus/syntax": "^0.3.6"
|
||||
"@wrnexus/core": "^0.7.0",
|
||||
"@wrnexus/router": "^0.7.0",
|
||||
"@wrnexus/csr": "^0.7.0",
|
||||
"@wrnexus/compiler": "^0.7.0",
|
||||
"@wrnexus/styles": "^0.7.0",
|
||||
"@wrnexus/dev-server": "^0.7.0",
|
||||
"@wrnexus/ui": "^0.7.0",
|
||||
"@wrnexus/validation": "^0.7.0",
|
||||
"@wrnexus/i18n": "^0.7.0",
|
||||
"@wrnexus/db": "^0.7.0",
|
||||
"@wrnexus/plugin": "^0.7.0",
|
||||
"@wrnexus/syntax": "^0.7.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -164,8 +164,8 @@ A file opens with `page <Name>` or `component <Name>` followed by a `{ ... }` bo
|
||||
- `types { <TypeScript declarations> }` — reusable interfaces and aliases for the current file.
|
||||
- `props { name: Type = <default> ... }` — typed component props. Omit `= <default>` to make a prop required. Legacy inferred props remain supported.
|
||||
- `@event name = function` inside `props` — declares a public component event. Emit it from component behavior with `name(detail)` or `$emit("name", detail)`, and consume it with `<Component @name="handler(event)" />`.
|
||||
- `state <ident>: Type = <expr>` — typed reactive state seeded from a raw JS expression. The annotation is optional for backward compatibility.
|
||||
- `view { <html> }` — plain HTML with `{expr}` interpolation in text and attributes, hyphenated attributes, boolean attributes, `@event="..."` client bindings, and `<!-- comments -->`. Attribute expressions that reference `state` keep an SSR value and update reactively in the browser.
|
||||
- `state <ident>: Type = <expr>` — typed reactive state seeded from a raw JS expression, including native array and object literals. The annotation is optional for backward compatibility.
|
||||
- `view { <html> }` — plain HTML with `{expr}` interpolation in text and attributes, JSX-style component props such as `items={items}`, `items={[...]}`, and `options={{...}}`, hyphenated attributes, boolean attributes, `@event="..."` client bindings, and `<!-- comments -->`. Structured component props are serialized safely for SSR; expressions that reference `state` retain their initial value and update reactively in the browser.
|
||||
- `seo { key = "value" ... }` — metadata merged into the generated `meta`.
|
||||
- `style { <raw css> }` — inlined page/component stylesheet (repeatable).
|
||||
- `functions { <TypeScript> }` — helpers with typed parameters and return values. Types remain in server output and are safely erased from browser behavior code.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/compiler",
|
||||
"version": "0.3.6",
|
||||
"version": "0.7.0",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/compiler — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -21,7 +21,8 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/syntax": "^0.3.6"
|
||||
"@wrnexus/syntax": "^0.7.0",
|
||||
"@wrnexus/store": "^0.7.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/core",
|
||||
"version": "0.3.6",
|
||||
"version": "0.7.0",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/core — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/csr",
|
||||
"version": "0.3.6",
|
||||
"version": "0.7.0",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/csr — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -21,7 +21,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "^0.3.6"
|
||||
"@wrnexus/core": "^0.7.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/db",
|
||||
"version": "0.3.6",
|
||||
"version": "0.7.0",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/db — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/dev-server",
|
||||
"version": "0.3.6",
|
||||
"version": "0.7.0",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/dev-server — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -25,20 +25,23 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "^0.3.6",
|
||||
"@wrnexus/dev-toolbar": "^0.3.6",
|
||||
"@wrnexus/router": "^0.3.6",
|
||||
"@wrnexus/ssr": "^0.3.6",
|
||||
"@wrnexus/csr": "^0.3.6",
|
||||
"@wrnexus/compiler": "^0.3.6",
|
||||
"@wrnexus/styles": "^0.3.6",
|
||||
"@wrnexus/ui": "^0.3.6",
|
||||
"@wrnexus/validation": "^0.3.6",
|
||||
"@wrnexus/i18n": "^0.3.6",
|
||||
"@wrnexus/db": "^0.3.6",
|
||||
"@wrnexus/pubsub": "^0.3.6",
|
||||
"@wrnexus/uploader": "^0.3.6",
|
||||
"@wrnexus/plugin": "^0.3.6"
|
||||
"@wrnexus/core": "^0.7.0",
|
||||
"@wrnexus/dev-toolbar": "^0.7.0",
|
||||
"@wrnexus/router": "^0.7.0",
|
||||
"@wrnexus/ssr": "^0.7.0",
|
||||
"@wrnexus/csr": "^0.7.0",
|
||||
"@wrnexus/compiler": "^0.7.0",
|
||||
"@wrnexus/styles": "^0.7.0",
|
||||
"@wrnexus/ui": "^0.7.0",
|
||||
"@wrnexus/validation": "^0.7.0",
|
||||
"@wrnexus/i18n": "^0.7.0",
|
||||
"@wrnexus/db": "^0.7.0",
|
||||
"@wrnexus/pubsub": "^0.7.0",
|
||||
"@wrnexus/uploader": "^0.7.0",
|
||||
"@wrnexus/plugin": "^0.7.0",
|
||||
"@wrnexus/store": "^0.7.0",
|
||||
"@wrnexus/security": "^0.7.0",
|
||||
"@wrnexus/observability": "^0.7.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/dev-toolbar",
|
||||
"version": "0.3.6",
|
||||
"version": "0.7.0",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/dev-toolbar — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/encryption",
|
||||
"version": "0.3.6",
|
||||
"version": "0.7.0",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/encryption — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/helpers",
|
||||
"version": "0.3.6",
|
||||
"version": "0.7.0",
|
||||
"type": "module",
|
||||
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
|
||||
"license": "MIT",
|
||||
@@ -21,7 +21,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "^0.3.6"
|
||||
"@wrnexus/core": "^0.7.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/i18n",
|
||||
"version": "0.3.6",
|
||||
"version": "0.7.0",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/i18n — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -21,7 +21,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "^0.3.6"
|
||||
"@wrnexus/core": "^0.7.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/jwt",
|
||||
"version": "0.3.6",
|
||||
"version": "0.7.0",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/jwt — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/mobile",
|
||||
"version": "0.3.6",
|
||||
"version": "0.7.0",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/mobile — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -21,7 +21,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/native": "^0.3.6"
|
||||
"@wrnexus/native": "^0.7.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/native",
|
||||
"version": "0.3.6",
|
||||
"version": "0.7.0",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/native — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/oauth",
|
||||
"version": "0.3.6",
|
||||
"version": "0.7.0",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/oauth — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/plugin",
|
||||
"version": "0.3.6",
|
||||
"version": "0.7.0",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/plugin — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -18,10 +18,22 @@
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./types": {
|
||||
"types": "./dist/types.d.ts",
|
||||
"import": "./dist/types.js"
|
||||
},
|
||||
"./manifest": {
|
||||
"types": "./dist/manifest.d.ts",
|
||||
"import": "./dist/manifest.js"
|
||||
},
|
||||
"./discovery": {
|
||||
"types": "./dist/discovery.d.ts",
|
||||
"import": "./dist/discovery.js"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/syntax": "^0.3.6"
|
||||
"@wrnexus/syntax": "^0.7.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/pubsub",
|
||||
"version": "0.3.6",
|
||||
"version": "0.7.0",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/pubsub — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/queue",
|
||||
"version": "0.3.6",
|
||||
"version": "0.7.0",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/queue — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/reactive",
|
||||
"version": "0.3.6",
|
||||
"version": "0.7.0",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/reactive — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/router",
|
||||
"version": "0.3.6",
|
||||
"version": "0.7.0",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/router — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -21,8 +21,8 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/compiler": "^0.3.6",
|
||||
"@wrnexus/core": "^0.3.6"
|
||||
"@wrnexus/compiler": "^0.7.0",
|
||||
"@wrnexus/core": "^0.7.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ssr",
|
||||
"version": "0.3.6",
|
||||
"version": "0.7.0",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/ssr — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -18,10 +18,20 @@
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./rpc": {
|
||||
"types": "./dist/rpc.d.ts",
|
||||
"import": "./dist/rpc.js"
|
||||
},
|
||||
"./store-context": {
|
||||
"types": "./dist/store-context.d.ts",
|
||||
"import": "./dist/store-context.js"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "^0.3.6"
|
||||
"@wrnexus/core": "^0.7.0",
|
||||
"@wrnexus/store": "^0.7.0",
|
||||
"@wrnexus/security": "^0.7.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/styles",
|
||||
"version": "0.3.6",
|
||||
"version": "0.7.0",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/styles — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -21,9 +21,9 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/uploader": "^0.3.6",
|
||||
"@wrnexus/core": "^0.3.6",
|
||||
"@wrnexus/plugin": "^0.3.6"
|
||||
"@wrnexus/uploader": "^0.7.0",
|
||||
"@wrnexus/core": "^0.7.0",
|
||||
"@wrnexus/plugin": "^0.7.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/syntax",
|
||||
"version": "0.3.6",
|
||||
"version": "0.7.0",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/syntax — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/test",
|
||||
"version": "0.3.6",
|
||||
"version": "0.7.0",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/test — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/tracking",
|
||||
"version": "0.3.6",
|
||||
"version": "0.7.0",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/tracking — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
+408
-195
File diff suppressed because it is too large
Load Diff
+14
-12
@@ -15,7 +15,7 @@ driven by `var(--wire-*)` theme tokens, so components restyle instantly when the
|
||||
theme changes. The tiny JS surface (`src/index.ts`) exists only so the toolchain
|
||||
(CLI build + dev server) can locate the component directory and stylesheet.
|
||||
|
||||
The complete PDF-aligned catalog currently contains **891 components**. The
|
||||
The complete PDF-aligned catalog currently contains **85 components**. The
|
||||
generated `COMPONENTS.md` and `component-reference.json` files document every
|
||||
mount name, prop, inferred type, default/required status, slot, event, category,
|
||||
and source file directly from the packaged `.wrn` source.
|
||||
@@ -35,10 +35,11 @@ In practice you rarely install this directly: `@wrnexus/cli` and
|
||||
|
||||
## Components
|
||||
|
||||
Components live as `.wrn` files under `packages/ui/components/`. The mount name
|
||||
is the **lowercase file basename** (e.g. `button.wrn` → `data-component="button"`).
|
||||
Each accepts a `class` prop (appended to its root element) and most render their
|
||||
body from either a named prop or the default slot.
|
||||
Components live as `.wrn` files under `packages/ui/components/`. The canonical mount
|
||||
name comes from the component declaration (for example, `component Button` mounts as
|
||||
`data-component="Button"`). Component lookup is case-insensitive, so existing lowercase
|
||||
mounts continue to work. Each component accepts a `class` prop (appended to its root
|
||||
element), and most render their body from either a named prop or the default slot.
|
||||
|
||||
### Layout
|
||||
|
||||
@@ -91,16 +92,17 @@ equivalent is exported as `@wrnexus/ui/component-reference.json`.
|
||||
|
||||
## API
|
||||
|
||||
The JS module (`@wrnexus/ui`) exposes four helpers used by the build tooling to
|
||||
The JS module (`@wrnexus/ui`) exposes five helpers used by the build tooling to
|
||||
locate the component assets. There is no component code to import — the components
|
||||
are `.wrn` files rendered server-side.
|
||||
|
||||
| Export | Signature | Returns |
|
||||
| ------------------ | ---------------- | ------------------------------------------------------------------------------------------ |
|
||||
| `uiComponentsDir` | `() => string` | Absolute path to the `.wrn` component directory (feed to `buildRouter`'s `componentDirs`). |
|
||||
| `uiCssPath` | `() => string` | Absolute path to `ui.css`. |
|
||||
| `uiCss` | `() => string` | The `ui.css` file contents (all `.wire-*` classes, themed via tokens). |
|
||||
| `uiComponentNames` | `() => string[]` | Sorted list of built-in component names (e.g. for `wrnexus eject` listing). |
|
||||
| Export | Signature | Returns |
|
||||
| ------------------ | -------------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| `uiComponentsDir` | `() => string` | Absolute path to the `.wrn` component directory (feed to `buildRouter`'s `componentDirs`). |
|
||||
| `uiCssPath` | `() => string` | Absolute path to `ui.css`. |
|
||||
| `uiCss` | `() => string` | The `ui.css` file contents (all `.wire-*` classes, themed via tokens). |
|
||||
| `uiComponentNames` | `() => string[]` | Sorted list of declared built-in component names. |
|
||||
| `uiComponentPath` | `(name: string) => string` | Absolute source path for a declared component name or case-insensitive alias. |
|
||||
|
||||
### `./ui.css` asset export
|
||||
|
||||
|
||||
+420
-309
@@ -1,66 +1,46 @@
|
||||
{
|
||||
"sourceDocuments": ["Screenshot-directed WRNexus UI catalog reset"],
|
||||
"components": [
|
||||
{
|
||||
"name": "Container",
|
||||
"category": "layout",
|
||||
"purpose": "Theme-aware, responsive container component."
|
||||
},
|
||||
{
|
||||
"name": "Columns",
|
||||
"category": "layout",
|
||||
"purpose": "Theme-aware, responsive columns component."
|
||||
},
|
||||
{
|
||||
"name": "Grid",
|
||||
"category": "layout",
|
||||
"purpose": "Theme-aware, responsive grid component."
|
||||
},
|
||||
{
|
||||
"name": "LayoutSplitter",
|
||||
"category": "layout",
|
||||
"purpose": "Theme-aware, responsive layout splitter component."
|
||||
},
|
||||
{
|
||||
"name": "Typography",
|
||||
"category": "layout",
|
||||
"purpose": "Theme-aware, responsive typography component."
|
||||
},
|
||||
{
|
||||
"name": "Image",
|
||||
"category": "layout",
|
||||
"purpose": "Theme-aware, responsive image component."
|
||||
},
|
||||
{
|
||||
"name": "Link",
|
||||
"category": "layout",
|
||||
"purpose": "Theme-aware, responsive link component."
|
||||
},
|
||||
{
|
||||
"name": "Divider",
|
||||
"category": "layout",
|
||||
"purpose": "Theme-aware, responsive divider component."
|
||||
},
|
||||
{
|
||||
"name": "Kbd",
|
||||
"category": "layout",
|
||||
"purpose": "Theme-aware, responsive kbd component."
|
||||
},
|
||||
{
|
||||
"name": "CustomScrollbar",
|
||||
"category": "layout",
|
||||
"purpose": "Theme-aware, responsive custom scrollbar component."
|
||||
},
|
||||
{
|
||||
"name": "Accordion",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive accordion component."
|
||||
},
|
||||
{
|
||||
"name": "AdvancedDatePicker",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive advanced date picker component."
|
||||
},
|
||||
{
|
||||
"name": "AdvancedRangeSlider",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive advanced range slider component."
|
||||
},
|
||||
{
|
||||
"name": "AdvancedSelect",
|
||||
"category": "advanced-forms",
|
||||
"purpose": "Theme-aware, responsive advanced select component."
|
||||
},
|
||||
{
|
||||
"name": "Alert",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive alert component."
|
||||
},
|
||||
{
|
||||
"name": "AnnouncementBar",
|
||||
"category": "marketing",
|
||||
"purpose": "Publish a responsive notice with badge, icon, supporting copy, action, dismiss behavior, and width controls."
|
||||
},
|
||||
{
|
||||
"name": "AuthForm",
|
||||
"category": "core",
|
||||
"purpose": "Reusable auth form component."
|
||||
},
|
||||
{
|
||||
"name": "AuthSplitLayout",
|
||||
"category": "core",
|
||||
"purpose": "Reusable auth split layout component."
|
||||
},
|
||||
{
|
||||
"name": "Avatar",
|
||||
"category": "base",
|
||||
@@ -71,6 +51,11 @@
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive avatar group component."
|
||||
},
|
||||
{
|
||||
"name": "BackToTop",
|
||||
"category": "navigation",
|
||||
"purpose": "Provide a responsive floating control that returns long pages to the top and can show scroll progress."
|
||||
},
|
||||
{
|
||||
"name": "Badge",
|
||||
"category": "base",
|
||||
@@ -81,6 +66,11 @@
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive blockquote component."
|
||||
},
|
||||
{
|
||||
"name": "Breadcrumb",
|
||||
"category": "navigation",
|
||||
"purpose": "Show responsive hierarchical navigation with home support, separators, current-page state, sizes, and selection events."
|
||||
},
|
||||
{
|
||||
"name": "Button",
|
||||
"category": "base",
|
||||
@@ -92,14 +82,14 @@
|
||||
"purpose": "Theme-aware, responsive button group component."
|
||||
},
|
||||
{
|
||||
"name": "Card",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive card component."
|
||||
"name": "CTASection",
|
||||
"category": "marketing",
|
||||
"purpose": "Close a page or major section with conversion-focused copy, actions, and optional supporting visual content."
|
||||
},
|
||||
{
|
||||
"name": "ChatBubble",
|
||||
"name": "Card",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive chat bubble component."
|
||||
"purpose": "Group related content in a responsive themed surface with title, description, content, and supporting slots."
|
||||
},
|
||||
{
|
||||
"name": "Carousel",
|
||||
@@ -107,149 +97,14 @@
|
||||
"purpose": "Theme-aware, responsive carousel component."
|
||||
},
|
||||
{
|
||||
"name": "Collapse",
|
||||
"name": "Chart",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive chart component."
|
||||
},
|
||||
{
|
||||
"name": "ChatBubble",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive collapse component."
|
||||
},
|
||||
{
|
||||
"name": "DatePicker",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive date picker component."
|
||||
},
|
||||
{
|
||||
"name": "DeviceFrame",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive device frame component."
|
||||
},
|
||||
{
|
||||
"name": "List",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive list component."
|
||||
},
|
||||
{
|
||||
"name": "ListGroup",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive list group component."
|
||||
},
|
||||
{
|
||||
"name": "LegendIndicator",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive legend indicator component."
|
||||
},
|
||||
{
|
||||
"name": "Progress",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive progress component."
|
||||
},
|
||||
{
|
||||
"name": "FileUploadProgress",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive file upload progress component."
|
||||
},
|
||||
{
|
||||
"name": "Rating",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive rating component."
|
||||
},
|
||||
{
|
||||
"name": "Skeleton",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive skeleton component."
|
||||
},
|
||||
{
|
||||
"name": "Spinner",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive spinner component."
|
||||
},
|
||||
{
|
||||
"name": "StyledIcon",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive styled icon component."
|
||||
},
|
||||
{
|
||||
"name": "Toast",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive toast component."
|
||||
},
|
||||
{
|
||||
"name": "Timeline",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive timeline component."
|
||||
},
|
||||
{
|
||||
"name": "TreeView",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive tree view component."
|
||||
},
|
||||
{
|
||||
"name": "Marquee",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive marquee component."
|
||||
},
|
||||
{
|
||||
"name": "Navbar",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive navbar component."
|
||||
},
|
||||
{
|
||||
"name": "MegaMenu",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive mega menu component."
|
||||
},
|
||||
{
|
||||
"name": "Nav",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive nav component."
|
||||
},
|
||||
{
|
||||
"name": "Tabs",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive tabs component."
|
||||
},
|
||||
{
|
||||
"name": "Sidebar",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive sidebar component."
|
||||
},
|
||||
{
|
||||
"name": "Scrollspy",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive scrollspy component."
|
||||
},
|
||||
{
|
||||
"name": "Breadcrumb",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive breadcrumb component."
|
||||
},
|
||||
{
|
||||
"name": "Pagination",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive pagination component."
|
||||
},
|
||||
{
|
||||
"name": "Stepper",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive stepper component."
|
||||
},
|
||||
{
|
||||
"name": "Input",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive input component."
|
||||
},
|
||||
{
|
||||
"name": "InputGroup",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive input group component."
|
||||
},
|
||||
{
|
||||
"name": "Textarea",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive textarea component."
|
||||
},
|
||||
{
|
||||
"name": "FileInput",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive file input component."
|
||||
"purpose": "Theme-aware, responsive chat bubble component."
|
||||
},
|
||||
{
|
||||
"name": "Checkbox",
|
||||
@@ -257,24 +112,14 @@
|
||||
"purpose": "Theme-aware, responsive checkbox component."
|
||||
},
|
||||
{
|
||||
"name": "Radio",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive radio component."
|
||||
"name": "Clipboard",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive clipboard component."
|
||||
},
|
||||
{
|
||||
"name": "Switch",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive switch component."
|
||||
},
|
||||
{
|
||||
"name": "Select",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive select component."
|
||||
},
|
||||
{
|
||||
"name": "RangeSlider",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive range slider component."
|
||||
"name": "Collapse",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive collapse component."
|
||||
},
|
||||
{
|
||||
"name": "ColorPicker",
|
||||
@@ -282,47 +127,29 @@
|
||||
"purpose": "Theme-aware, responsive color picker component."
|
||||
},
|
||||
{
|
||||
"name": "TimePicker",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive time picker component."
|
||||
},
|
||||
{
|
||||
"name": "AdvancedSelect",
|
||||
"category": "advanced-forms",
|
||||
"purpose": "Theme-aware, responsive advanced select component.",
|
||||
"events": ["search", "select", "change", "clear", "open", "close", "load", "error"]
|
||||
"name": "Columns",
|
||||
"category": "layout",
|
||||
"purpose": "Create responsive balanced content columns with configurable count, gap, density, and maximum width."
|
||||
},
|
||||
{
|
||||
"name": "ComboBox",
|
||||
"category": "advanced-forms",
|
||||
"purpose": "Editable autocomplete combobox with local and remote suggestions.",
|
||||
"events": ["search", "select", "change", "clear", "open", "close", "load", "error"]
|
||||
"purpose": "Editable autocomplete combobox with local and remote suggestions."
|
||||
},
|
||||
{
|
||||
"name": "SearchBox",
|
||||
"category": "advanced-forms",
|
||||
"purpose": "Theme-aware, responsive search box component."
|
||||
"name": "Confetti",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive confetti component."
|
||||
},
|
||||
{
|
||||
"name": "InputNumber",
|
||||
"category": "advanced-forms",
|
||||
"purpose": "Theme-aware, responsive input number component."
|
||||
"name": "Container",
|
||||
"category": "layout",
|
||||
"purpose": "Constrain and align page content with responsive gutters and compact, wide, or full width options."
|
||||
},
|
||||
{
|
||||
"name": "StrongPassword",
|
||||
"category": "advanced-forms",
|
||||
"purpose": "Theme-aware, responsive strong password component."
|
||||
},
|
||||
{
|
||||
"name": "TogglePassword",
|
||||
"category": "advanced-forms",
|
||||
"purpose": "Accessible password field with optional show and hide controls.",
|
||||
"events": ["input", "change", "toggle"]
|
||||
},
|
||||
{
|
||||
"name": "ToggleCount",
|
||||
"category": "advanced-forms",
|
||||
"purpose": "Theme-aware, responsive toggle count component."
|
||||
"name": "ContextMenu",
|
||||
"category": "overlays",
|
||||
"purpose": "Open an accessible keyboard-aware action menu from pointer or keyboard context interactions."
|
||||
},
|
||||
{
|
||||
"name": "CopyMarkup",
|
||||
@@ -330,70 +157,9 @@
|
||||
"purpose": "Theme-aware, responsive copy markup component."
|
||||
},
|
||||
{
|
||||
"name": "PinInput",
|
||||
"category": "advanced-forms",
|
||||
"purpose": "Secure multi-cell PIN and verification-code input with regex and paste support.",
|
||||
"events": ["input", "change", "complete", "paste", "clear", "error"]
|
||||
},
|
||||
{
|
||||
"name": "Dropdown",
|
||||
"category": "overlays",
|
||||
"purpose": "Theme-aware, responsive dropdown component."
|
||||
},
|
||||
{
|
||||
"name": "ContextMenu",
|
||||
"category": "overlays",
|
||||
"purpose": "Theme-aware, responsive context menu component."
|
||||
},
|
||||
{
|
||||
"name": "Modal",
|
||||
"category": "overlays",
|
||||
"purpose": "Theme-aware, responsive modal component."
|
||||
},
|
||||
{
|
||||
"name": "Drawer",
|
||||
"category": "overlays",
|
||||
"purpose": "Theme-aware, responsive drawer component."
|
||||
},
|
||||
{
|
||||
"name": "Popover",
|
||||
"category": "overlays",
|
||||
"purpose": "Theme-aware, responsive popover component."
|
||||
},
|
||||
{
|
||||
"name": "Tooltip",
|
||||
"category": "overlays",
|
||||
"purpose": "Theme-aware, responsive tooltip component."
|
||||
},
|
||||
{
|
||||
"name": "Table",
|
||||
"category": "tables",
|
||||
"purpose": "Theme-aware, responsive table component."
|
||||
},
|
||||
{
|
||||
"name": "AdvancedRangeSlider",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive advanced range slider component."
|
||||
},
|
||||
{
|
||||
"name": "AdvancedDatePicker",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive advanced date picker component."
|
||||
},
|
||||
{
|
||||
"name": "Chart",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive chart component."
|
||||
},
|
||||
{
|
||||
"name": "Clipboard",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive clipboard component."
|
||||
},
|
||||
{
|
||||
"name": "Confetti",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive confetti component."
|
||||
"name": "CustomScrollbar",
|
||||
"category": "layout",
|
||||
"purpose": "Theme-aware, responsive custom scrollbar component."
|
||||
},
|
||||
{
|
||||
"name": "DataMap",
|
||||
@@ -405,26 +171,371 @@
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive data table component."
|
||||
},
|
||||
{
|
||||
"name": "DatePicker",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive date picker component."
|
||||
},
|
||||
{
|
||||
"name": "DeviceFrame",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive device frame component."
|
||||
},
|
||||
{
|
||||
"name": "Divider",
|
||||
"category": "layout",
|
||||
"purpose": "Separate related horizontal or vertical content with optional labels, sizes, and semantic colors."
|
||||
},
|
||||
{
|
||||
"name": "DragAndDrop",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive drag and drop component."
|
||||
},
|
||||
{
|
||||
"name": "Drawer",
|
||||
"category": "overlays",
|
||||
"purpose": "Present responsive modal side or bottom content with focus management, backdrop behavior, slots, and close events."
|
||||
},
|
||||
{
|
||||
"name": "Dropdown",
|
||||
"category": "overlays",
|
||||
"purpose": "Open an accessible anchored menu with keyboard navigation, item selection, actions, and responsive placement."
|
||||
},
|
||||
{
|
||||
"name": "FeatureCard",
|
||||
"category": "marketing",
|
||||
"purpose": "Present one linked feature or service with media, icon, badge, description, and action."
|
||||
},
|
||||
{
|
||||
"name": "FeatureGrid",
|
||||
"category": "layout",
|
||||
"purpose": "Arrange feature cards, services, solutions, or benefits in a responsive equal-height grid."
|
||||
},
|
||||
{
|
||||
"name": "FeatureIconCard",
|
||||
"category": "marketing",
|
||||
"purpose": "Present a compact feature or benefit with a styled icon, title, description, badge, and optional link."
|
||||
},
|
||||
{
|
||||
"name": "FileInput",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive file input component."
|
||||
},
|
||||
{
|
||||
"name": "FileUpload",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive file upload component."
|
||||
},
|
||||
{
|
||||
"name": "FileUploadProgress",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive file upload progress component."
|
||||
},
|
||||
{
|
||||
"name": "Footer",
|
||||
"category": "layout",
|
||||
"purpose": "Render structured responsive footer navigation, pre and post content, copyright content, links, and public events."
|
||||
},
|
||||
{
|
||||
"name": "Grid",
|
||||
"category": "layout",
|
||||
"purpose": "Arrange arbitrary content in a responsive configurable CSS grid with stable columns, gaps, alignment, and width."
|
||||
},
|
||||
{
|
||||
"name": "Hero",
|
||||
"category": "marketing",
|
||||
"purpose": "Build a full-width responsive hero with constrained content, actions, trust signals, and a structured or custom visual panel."
|
||||
},
|
||||
{
|
||||
"name": "HeroActions",
|
||||
"category": "marketing",
|
||||
"purpose": "Group hero and campaign actions with consistent alignment, orientation, sizing, and responsive mobile stacking."
|
||||
},
|
||||
{
|
||||
"name": "Image",
|
||||
"category": "layout",
|
||||
"purpose": "Render a responsive image with explicit dimensions, loading behavior, alternative text, sizing, and rounded treatment."
|
||||
},
|
||||
{
|
||||
"name": "Input",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive input component."
|
||||
},
|
||||
{
|
||||
"name": "InputGroup",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive input group component."
|
||||
},
|
||||
{
|
||||
"name": "InputNumber",
|
||||
"category": "advanced-forms",
|
||||
"purpose": "Theme-aware, responsive input number component."
|
||||
},
|
||||
{
|
||||
"name": "Kbd",
|
||||
"category": "layout",
|
||||
"purpose": "Theme-aware, responsive kbd component."
|
||||
},
|
||||
{
|
||||
"name": "LayoutSplitter",
|
||||
"category": "layout",
|
||||
"purpose": "Theme-aware, responsive layout splitter component."
|
||||
},
|
||||
{
|
||||
"name": "LegendIndicator",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive legend indicator component."
|
||||
},
|
||||
{
|
||||
"name": "Link",
|
||||
"category": "layout",
|
||||
"purpose": "Render an accessible internal or external link with target, relation, size, color, and public focus or click events."
|
||||
},
|
||||
{
|
||||
"name": "List",
|
||||
"category": "base",
|
||||
"purpose": "Present structured responsive linked or status items with icons, descriptions, actions, and selection events."
|
||||
},
|
||||
{
|
||||
"name": "ListGroup",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive list group component."
|
||||
},
|
||||
{
|
||||
"name": "Map",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive map component."
|
||||
"purpose": "Present responsive location information and markers with map-ready metadata and movement or marker events."
|
||||
},
|
||||
{
|
||||
"name": "MarketingSectionHeader",
|
||||
"category": "marketing",
|
||||
"purpose": "Introduce marketing content with an eyebrow, title, description, and optional linked action."
|
||||
},
|
||||
{
|
||||
"name": "Marquee",
|
||||
"category": "base",
|
||||
"purpose": "Continuously present responsive labels, partners, notices, or capabilities with pause and resume behavior."
|
||||
},
|
||||
{
|
||||
"name": "MegaMenu",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive mega menu component."
|
||||
},
|
||||
{
|
||||
"name": "MetricCard",
|
||||
"category": "data",
|
||||
"purpose": "Display one operational metric with value, suffix, description, icon, trend, progress, and optional action."
|
||||
},
|
||||
{
|
||||
"name": "MetricGrid",
|
||||
"category": "data",
|
||||
"purpose": "Arrange operational metrics, KPIs, public statistics, or service indicators in a responsive equal-height grid."
|
||||
},
|
||||
{
|
||||
"name": "Modal",
|
||||
"category": "overlays",
|
||||
"purpose": "Present an accessible modal dialog with focus management, confirmation, cancellation, slots, and responsive sizing."
|
||||
},
|
||||
{
|
||||
"name": "Nav",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive nav component."
|
||||
},
|
||||
{
|
||||
"name": "Navbar",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive navbar component."
|
||||
},
|
||||
{
|
||||
"name": "PageHeader",
|
||||
"category": "marketing",
|
||||
"purpose": "Introduce an internal or public page with breadcrumbs, icon, title, description, and primary or secondary actions."
|
||||
},
|
||||
{
|
||||
"name": "Pagination",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive pagination component."
|
||||
},
|
||||
{
|
||||
"name": "PinInput",
|
||||
"category": "advanced-forms",
|
||||
"purpose": "Secure multi-cell PIN and verification-code input with regex and paste support."
|
||||
},
|
||||
{
|
||||
"name": "Popover",
|
||||
"category": "overlays",
|
||||
"purpose": "Display anchored supporting content with configurable trigger, placement, responsive sizing, and open or close events."
|
||||
},
|
||||
{
|
||||
"name": "PortalDashboard",
|
||||
"category": "core",
|
||||
"purpose": "Reusable portal dashboard component."
|
||||
},
|
||||
{
|
||||
"name": "PreferenceSwitcher",
|
||||
"category": "core",
|
||||
"purpose": "Reusable preference switcher component."
|
||||
},
|
||||
{
|
||||
"name": "Progress",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive progress component."
|
||||
},
|
||||
{
|
||||
"name": "PublicPageShell",
|
||||
"category": "layout",
|
||||
"purpose": "Provide the outer responsive structure, width, background, slots, overflow, and minimum-height behavior for public pages."
|
||||
},
|
||||
{
|
||||
"name": "Radio",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive radio component."
|
||||
},
|
||||
{
|
||||
"name": "RangeSlider",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive range slider component."
|
||||
},
|
||||
{
|
||||
"name": "Rating",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive rating component."
|
||||
},
|
||||
{
|
||||
"name": "Scrollspy",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive scrollspy component."
|
||||
},
|
||||
{
|
||||
"name": "SearchBox",
|
||||
"category": "forms",
|
||||
"purpose": "Provide an accessible responsive search field with labels, validation states, sizes, and input or change events."
|
||||
},
|
||||
{
|
||||
"name": "Section",
|
||||
"category": "layout",
|
||||
"purpose": "Create a responsive themed page section with controlled spacing, width, borders, and surface treatment."
|
||||
},
|
||||
{
|
||||
"name": "SectionHeader",
|
||||
"category": "layout",
|
||||
"purpose": "Introduce a section with an eyebrow, title, description, alignment, and responsive heading hierarchy."
|
||||
},
|
||||
{
|
||||
"name": "Select",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive select component."
|
||||
},
|
||||
{
|
||||
"name": "Sidebar",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive sidebar component."
|
||||
},
|
||||
{
|
||||
"name": "Skeleton",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive skeleton component."
|
||||
},
|
||||
{
|
||||
"name": "Spinner",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive spinner component."
|
||||
},
|
||||
{
|
||||
"name": "SplitHero",
|
||||
"category": "marketing",
|
||||
"purpose": "Build a responsive two-column introduction balancing descriptive content with a visual, image, or structured data panel."
|
||||
},
|
||||
{
|
||||
"name": "StatsBar",
|
||||
"category": "data",
|
||||
"purpose": "Present a compact responsive strip of key facts, counts, performance indicators, or trust signals."
|
||||
},
|
||||
{
|
||||
"name": "Stepper",
|
||||
"category": "navigation",
|
||||
"purpose": "Theme-aware, responsive stepper component."
|
||||
},
|
||||
{
|
||||
"name": "StrongPassword",
|
||||
"category": "advanced-forms",
|
||||
"purpose": "Theme-aware, responsive strong password component."
|
||||
},
|
||||
{
|
||||
"name": "StyledIcon",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive styled icon component."
|
||||
},
|
||||
{
|
||||
"name": "Switch",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive switch component."
|
||||
},
|
||||
{
|
||||
"name": "Table",
|
||||
"category": "tables",
|
||||
"purpose": "Theme-aware, responsive table component."
|
||||
},
|
||||
{
|
||||
"name": "Tabs",
|
||||
"category": "navigation",
|
||||
"purpose": "Switch between related responsive content panels with horizontal or vertical orientation and selection events."
|
||||
},
|
||||
{
|
||||
"name": "TextLink",
|
||||
"category": "marketing",
|
||||
"purpose": "Render an accessible text action with optional icon, arrow, underline, external state, and semantic styling."
|
||||
},
|
||||
{
|
||||
"name": "Textarea",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive textarea component."
|
||||
},
|
||||
{
|
||||
"name": "TimePicker",
|
||||
"category": "forms",
|
||||
"purpose": "Theme-aware, responsive time picker component."
|
||||
},
|
||||
{
|
||||
"name": "Timeline",
|
||||
"category": "base",
|
||||
"purpose": "Present responsive chronological activity, milestones, or workflow status with rich item metadata."
|
||||
},
|
||||
{
|
||||
"name": "Toast",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive toast component."
|
||||
},
|
||||
{
|
||||
"name": "ToastNotifications",
|
||||
"category": "integrations",
|
||||
"purpose": "Theme-aware, responsive toast notifications component."
|
||||
},
|
||||
{
|
||||
"name": "ToggleCount",
|
||||
"category": "advanced-forms",
|
||||
"purpose": "Theme-aware, responsive toggle count component."
|
||||
},
|
||||
{
|
||||
"name": "TogglePassword",
|
||||
"category": "advanced-forms",
|
||||
"purpose": "Accessible password field with optional show and hide controls."
|
||||
},
|
||||
{
|
||||
"name": "Tooltip",
|
||||
"category": "overlays",
|
||||
"purpose": "Show concise accessible contextual help on hover, focus, click, or controlled open state."
|
||||
},
|
||||
{
|
||||
"name": "TreeView",
|
||||
"category": "base",
|
||||
"purpose": "Theme-aware, responsive tree view component."
|
||||
},
|
||||
{
|
||||
"name": "Typography",
|
||||
"category": "layout",
|
||||
"purpose": "Apply consistent readable responsive typography, widths, columns, spacing, and editorial hierarchy."
|
||||
},
|
||||
{
|
||||
"name": "WysiwygEditor",
|
||||
"category": "integrations",
|
||||
|
||||
+9187
-1374
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,206 @@
|
||||
component Accordion {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
items = []
|
||||
multiple = false
|
||||
class = ""
|
||||
outputs {
|
||||
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
open(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
close(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
}
|
||||
|
||||
props {
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
variant: string = "default"
|
||||
class: string = ""
|
||||
|
||||
id: string = "accordion"
|
||||
items: unknown[] = []
|
||||
defaultOpen: unknown[] = []
|
||||
multiple: boolean = false
|
||||
alwaysOpen: boolean = false
|
||||
disabled: boolean = false
|
||||
|
||||
indicator: string = "plus"
|
||||
indicatorPosition: string = "start"
|
||||
showIndicator: boolean = true
|
||||
bordered: boolean = false
|
||||
separated: boolean = false
|
||||
flush: boolean = false
|
||||
contentItalic: boolean = false
|
||||
|
||||
}
|
||||
|
||||
state openValues = defaultOpen
|
||||
|
||||
functions {
|
||||
shared function itemValue(item, index) {
|
||||
return item.value !== undefined && item.value !== ""
|
||||
? String(item.value)
|
||||
: String(index)
|
||||
}
|
||||
|
||||
shared function nestedValue(parent, parentIndex, item, index) {
|
||||
return itemValue(parent, parentIndex) + "." + itemValue(item, index)
|
||||
}
|
||||
|
||||
shared function isOpen(value) {
|
||||
return openValues.includes(value)
|
||||
}
|
||||
|
||||
shared function allowsMultiple() {
|
||||
return multiple || alwaysOpen
|
||||
}
|
||||
|
||||
client function dispatchAccordionEvent(sourceEvent, eventName, value, item, root, customEvent) {
|
||||
root = sourceEvent.currentTarget.closest("[data-wrn-accordion]")
|
||||
|
||||
if (!root) {
|
||||
return
|
||||
}
|
||||
|
||||
customEvent = document.createEvent("CustomEvent")
|
||||
customEvent.initCustomEvent(eventName, true, false, {
|
||||
component: "Accordion",
|
||||
value: value,
|
||||
item: item,
|
||||
open: isOpen(value),
|
||||
openValues: openValues
|
||||
})
|
||||
root.dispatchEvent(customEvent)
|
||||
}
|
||||
|
||||
client function toggleItem(sourceEvent, value, item, wasOpen) {
|
||||
if (disabled || item.disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
wasOpen = isOpen(value)
|
||||
|
||||
if (wasOpen) {
|
||||
openValues = openValues.filter((entry) => entry !== value)
|
||||
} else if (allowsMultiple()) {
|
||||
openValues = openValues.concat([value])
|
||||
} else {
|
||||
openValues = [value]
|
||||
}
|
||||
|
||||
dispatchAccordionEvent(
|
||||
sourceEvent,
|
||||
wasOpen ? "close" : "open",
|
||||
value,
|
||||
item
|
||||
)
|
||||
dispatchAccordionEvent(sourceEvent, "change", value, item)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--accordion {class}">
|
||||
{#each items as item}<details open="{item.open}"><summary>{item.label}</summary><div>{item.content}</div></details>{/each}
|
||||
<slot />
|
||||
<div
|
||||
{...attrs}
|
||||
id="{id}"
|
||||
data-wrn-accordion
|
||||
data-variant="{variant}"
|
||||
data-indicator="{indicator}"
|
||||
data-multiple="{allowsMultiple() ? 'true' : 'false'}"
|
||||
class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--accordion wire-next--accordion-{variant} {bordered ? 'wire-next--accordion-bordered' : ''} {separated ? 'wire-next--accordion-separated' : ''} {flush ? 'wire-next--accordion-flush' : ''} {disabled ? 'wire-next--disabled' : ''} {class}"
|
||||
>
|
||||
{#each items as item, index}
|
||||
<section
|
||||
class="wire-next__accordion-item"
|
||||
data-open="{isOpen(itemValue(item, index)) ? 'true' : 'false'}"
|
||||
data-disabled="{disabled || item.disabled ? 'true' : 'false'}"
|
||||
>
|
||||
<h3 class="wire-next__accordion-heading">
|
||||
<button
|
||||
type="button"
|
||||
id="{id}-trigger-{index}"
|
||||
aria-expanded="{isOpen(itemValue(item, index)) ? 'true' : 'false'}"
|
||||
aria-controls="{id}-panel-{index}"
|
||||
disabled="{disabled || item.disabled}"
|
||||
@click="toggleItem(event, itemValue(item, index), item)"
|
||||
>
|
||||
{#if showIndicator && indicatorPosition === "start"}
|
||||
<span class="wire-next__accordion-indicator" aria-hidden="true">
|
||||
{#if indicator === "chevron"}
|
||||
<span class="icon-[lucide--chevron-down]"></span>
|
||||
{:else}
|
||||
<span>+</span>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
<span>{item.label || item.title}</span>
|
||||
{#if showIndicator && indicatorPosition === "end"}
|
||||
<span class="wire-next__accordion-indicator" aria-hidden="true">
|
||||
{#if indicator === "chevron"}
|
||||
<span class="icon-[lucide--chevron-down]"></span>
|
||||
{:else}
|
||||
<span>+</span>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
</h3>
|
||||
|
||||
<div
|
||||
id="{id}-panel-{index}"
|
||||
class="wire-next__accordion-panel"
|
||||
role="region"
|
||||
aria-labelledby="{id}-trigger-{index}"
|
||||
aria-hidden="{isOpen(itemValue(item, index)) ? 'false' : 'true'}"
|
||||
>
|
||||
<div>
|
||||
<div class="wire-next__accordion-content {contentItalic ? 'wire-next__accordion-content-italic' : ''}">
|
||||
{#if item.content}<p>{item.content}</p>{/if}
|
||||
|
||||
{#if item.children && item.children.length > 0}
|
||||
<div class="wire-next__accordion-nested">
|
||||
{#each item.children as child, childIndex}
|
||||
<section
|
||||
class="wire-next__accordion-item"
|
||||
data-open="{isOpen(nestedValue(item, index, child, childIndex)) ? 'true' : 'false'}"
|
||||
>
|
||||
<h4 class="wire-next__accordion-heading">
|
||||
<button
|
||||
type="button"
|
||||
id="{id}-trigger-{index}-{childIndex}"
|
||||
aria-expanded="{isOpen(nestedValue(item, index, child, childIndex)) ? 'true' : 'false'}"
|
||||
aria-controls="{id}-panel-{index}-{childIndex}"
|
||||
disabled="{disabled || child.disabled}"
|
||||
@click="toggleItem(event, nestedValue(item, index, child, childIndex), child)"
|
||||
>
|
||||
{#if showIndicator}
|
||||
<span class="wire-next__accordion-indicator" aria-hidden="true">
|
||||
{#if indicator === "chevron"}
|
||||
<span class="icon-[lucide--chevron-down]"></span>
|
||||
{:else}
|
||||
<span>+</span>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
<span>{child.label || child.title}</span>
|
||||
</button>
|
||||
</h4>
|
||||
<div
|
||||
id="{id}-panel-{index}-{childIndex}"
|
||||
class="wire-next__accordion-panel"
|
||||
role="region"
|
||||
aria-labelledby="{id}-trigger-{index}-{childIndex}"
|
||||
aria-hidden="{isOpen(nestedValue(item, index, child, childIndex)) ? 'false' : 'true'}"
|
||||
>
|
||||
<div>
|
||||
<div class="wire-next__accordion-content {contentItalic ? 'wire-next__accordion-content-italic' : ''}">
|
||||
<p>{child.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/each}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
component AdvancedDatePicker {
|
||||
outputs {
|
||||
input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
open(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
close(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
clear(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Advanced Date Picker"
|
||||
description = ""
|
||||
items = []
|
||||
variant = "default"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
title: string = "Advanced Date Picker"
|
||||
description: string = ""
|
||||
items: unknown[] = []
|
||||
variant: string = "default"
|
||||
class: string = ""
|
||||
}
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--advanced-date-picker wire-next--variant-{variant} {class}">
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
component AdvancedRangeSlider {
|
||||
outputs {
|
||||
input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
start(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
end(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Advanced Range Slider"
|
||||
description = ""
|
||||
items = []
|
||||
variant = "default"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
title: string = "Advanced Range Slider"
|
||||
description: string = ""
|
||||
items: unknown[] = []
|
||||
variant: string = "default"
|
||||
class: string = ""
|
||||
}
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--advanced-range-slider wire-next--variant-{variant} {class}">
|
||||
|
||||
@@ -1,82 +1,85 @@
|
||||
component AdvancedSelect {
|
||||
outputs {
|
||||
search(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
clear(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
open(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
close(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
load(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
error(payload: { error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "Advanced Select"
|
||||
name = ""
|
||||
value = ""
|
||||
values = []
|
||||
options = []
|
||||
groups = []
|
||||
placeholder = "Select an option"
|
||||
placeholderIcon = ""
|
||||
searchPlaceholder = "Search options…"
|
||||
multiple = false
|
||||
searchable = true
|
||||
defaultOpen = false
|
||||
clearable = true
|
||||
allowEmpty = true
|
||||
tags = false
|
||||
disabled = false
|
||||
required = false
|
||||
invalid = false
|
||||
validationMessage = ""
|
||||
helpText = ""
|
||||
loading = false
|
||||
loadingLabel = "Loading options…"
|
||||
emptyLabel = "No options found"
|
||||
selectedOptionsLabel = "Selected options"
|
||||
clearLabel = "Clear selection"
|
||||
createLabel = "Create"
|
||||
loadMoreLabel = "Load more"
|
||||
searchMode = "contains"
|
||||
searchFields = "label,description"
|
||||
minSearchLength = 0
|
||||
searchResultLimit = 0
|
||||
maxSelections = 0
|
||||
showCounter = false
|
||||
counterTemplate = "{selected} selected"
|
||||
optionTemplate = "default"
|
||||
selectedTemplate = "default"
|
||||
closeOnSelect = true
|
||||
scrollToSelected = true
|
||||
fixed = false
|
||||
placement = "bottom"
|
||||
remote = false
|
||||
remoteUrl = ""
|
||||
remoteQueryParam = "q"
|
||||
remoteDebounce = 250
|
||||
remoteAutoLoad = true
|
||||
infinite = false
|
||||
hasMore = false
|
||||
page = 1
|
||||
class = ""
|
||||
@event search = function
|
||||
@event select = function
|
||||
@event change = function
|
||||
@event clear = function
|
||||
@event open = function
|
||||
@event close = function
|
||||
@event load = function
|
||||
@event error = function
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
label: string = "Advanced Select"
|
||||
name: string = ""
|
||||
value: string = ""
|
||||
values: unknown[] = []
|
||||
options: unknown[] = []
|
||||
groups: unknown[] = []
|
||||
placeholder: string = "Select an option"
|
||||
placeholderIcon: string = ""
|
||||
searchPlaceholder: string = "Search options…"
|
||||
multiple: boolean = false
|
||||
searchable: boolean = true
|
||||
defaultOpen: boolean = false
|
||||
clearable: boolean = true
|
||||
allowEmpty: boolean = true
|
||||
tags: boolean = false
|
||||
disabled: boolean = false
|
||||
required: boolean = false
|
||||
invalid: boolean = false
|
||||
validationMessage: string = ""
|
||||
helpText: string = ""
|
||||
loading: boolean = false
|
||||
loadingLabel: string = "Loading options…"
|
||||
emptyLabel: string = "No options found"
|
||||
selectedOptionsLabel: string = "Selected options"
|
||||
clearLabel: string = "Clear selection"
|
||||
createLabel: string = "Create"
|
||||
loadMoreLabel: string = "Load more"
|
||||
searchMode: string = "contains"
|
||||
searchFields: string = "label,description"
|
||||
minSearchLength: number = 0
|
||||
searchResultLimit: number = 0
|
||||
maxSelections: number = 0
|
||||
showCounter: boolean = false
|
||||
counterTemplate: string = "{selected} selected"
|
||||
optionTemplate: string = "default"
|
||||
selectedTemplate: string = "default"
|
||||
closeOnSelect: boolean = true
|
||||
scrollToSelected: boolean = true
|
||||
fixed: boolean = false
|
||||
placement: string = "bottom"
|
||||
remote: boolean = false
|
||||
remoteUrl: string = ""
|
||||
remoteQueryParam: string = "q"
|
||||
remoteDebounce: number = 250
|
||||
remoteAutoLoad: boolean = true
|
||||
infinite: boolean = false
|
||||
hasMore: boolean = false
|
||||
page: number = 1
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
state open = defaultOpen
|
||||
state query = ""
|
||||
state activeIndex = -1
|
||||
state query: string = ""
|
||||
state activeIndex: number = -1
|
||||
state selectedValue = value
|
||||
state selectedValues = values
|
||||
|
||||
functions {
|
||||
function allOptions() {
|
||||
shared function allOptions() {
|
||||
return [...groups.flatMap((group) => group.options || []), ...options]
|
||||
}
|
||||
|
||||
function searchableText(option) {
|
||||
shared function searchableText(option) {
|
||||
return ((option.label || "") + " " + (option.description || "")).toLowerCase()
|
||||
}
|
||||
|
||||
function matches(option) {
|
||||
shared function matches(option) {
|
||||
if (!query || query.length < Number(minSearchLength || 0)) {
|
||||
return true
|
||||
}
|
||||
@@ -89,22 +92,22 @@ component AdvancedSelect {
|
||||
return searchableText(option).includes(query.toLowerCase())
|
||||
}
|
||||
|
||||
function matchingOptions(list) {
|
||||
shared function matchingOptions(list) {
|
||||
return list.filter((option) => matches(option))
|
||||
}
|
||||
|
||||
function visibleOptions(list) {
|
||||
shared function visibleOptions(list) {
|
||||
if (searchResultLimit > 0) {
|
||||
return matchingOptions(list).slice(0, Number(searchResultLimit))
|
||||
}
|
||||
return matchingOptions(list)
|
||||
}
|
||||
|
||||
function flatVisibleOptions() {
|
||||
shared function flatVisibleOptions() {
|
||||
return visibleOptions(allOptions())
|
||||
}
|
||||
|
||||
function isSelected(option) {
|
||||
shared function isSelected(option) {
|
||||
return (
|
||||
multiple
|
||||
? selectedValues.includes(option.value)
|
||||
@@ -112,15 +115,15 @@ component AdvancedSelect {
|
||||
)
|
||||
}
|
||||
|
||||
function selectedOptions() {
|
||||
shared function selectedOptions() {
|
||||
return allOptions().filter((option) => isSelected(option))
|
||||
}
|
||||
|
||||
function selectedCount() {
|
||||
shared function selectedCount() {
|
||||
return multiple ? selectedValues.length : selectedValue ? 1 : 0
|
||||
}
|
||||
|
||||
function counterText() {
|
||||
shared function counterText() {
|
||||
return (
|
||||
maxSelections
|
||||
? selectedCount() + " / " + maxSelections + " selected"
|
||||
@@ -128,7 +131,7 @@ component AdvancedSelect {
|
||||
)
|
||||
}
|
||||
|
||||
function selectedText() {
|
||||
shared function selectedText() {
|
||||
return (
|
||||
multiple
|
||||
? selectedValues.join(", ")
|
||||
@@ -138,11 +141,11 @@ component AdvancedSelect {
|
||||
)
|
||||
}
|
||||
|
||||
function triggerText() {
|
||||
shared function triggerText() {
|
||||
return selectedCount() ? selectedText() : placeholder
|
||||
}
|
||||
|
||||
function canSelect(option) {
|
||||
shared function canSelect(option) {
|
||||
if (option.disabled) {
|
||||
return false
|
||||
}
|
||||
@@ -155,7 +158,7 @@ component AdvancedSelect {
|
||||
return selectedCount() < maxSelections
|
||||
}
|
||||
|
||||
function chooseSingle(option) {
|
||||
shared function chooseSingle(option) {
|
||||
if (!canSelect(option)) {
|
||||
return
|
||||
}
|
||||
@@ -166,7 +169,7 @@ component AdvancedSelect {
|
||||
}
|
||||
}
|
||||
|
||||
function chooseMultiple(option) {
|
||||
shared function chooseMultiple(option) {
|
||||
if (!canSelect(option)) {
|
||||
return
|
||||
}
|
||||
@@ -178,7 +181,7 @@ component AdvancedSelect {
|
||||
query = ""
|
||||
}
|
||||
|
||||
function chooseOption(option) {
|
||||
shared function chooseOption(option) {
|
||||
if (multiple) {
|
||||
chooseMultiple(option)
|
||||
return
|
||||
@@ -186,7 +189,7 @@ component AdvancedSelect {
|
||||
chooseSingle(option)
|
||||
}
|
||||
|
||||
function clearSelection(event) {
|
||||
client function clearSelection(event) {
|
||||
event.stopPropagation()
|
||||
selectedValue = ""
|
||||
selectedValues = []
|
||||
@@ -194,7 +197,7 @@ component AdvancedSelect {
|
||||
open = false
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
shared function toggle() {
|
||||
if (disabled) {
|
||||
return;
|
||||
};
|
||||
@@ -202,14 +205,14 @@ component AdvancedSelect {
|
||||
activeIndex = open && flatVisibleOptions().length ? 0 : -1;
|
||||
}
|
||||
|
||||
function moveActive(direction) {
|
||||
shared function moveActive(direction) {
|
||||
if (!flatVisibleOptions().length) {
|
||||
return
|
||||
}
|
||||
activeIndex = (activeIndex + direction + flatVisibleOptions().length) % flatVisibleOptions().length
|
||||
}
|
||||
|
||||
function handleKeydown(event) {
|
||||
client function handleKeydown(event) {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
@@ -243,7 +246,7 @@ component AdvancedSelect {
|
||||
}
|
||||
}
|
||||
|
||||
function optionIndex(option) {
|
||||
shared function optionIndex(option) {
|
||||
return flatVisibleOptions().findIndex((item) => item.value === option.value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,154 @@
|
||||
component AvatarGroup {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Avatar Group"
|
||||
description = ""
|
||||
items = []
|
||||
variant = "default"
|
||||
class = ""
|
||||
outputs {
|
||||
overflow(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
|
||||
}
|
||||
|
||||
props {
|
||||
items: unknown[] = []
|
||||
size: string = "md"
|
||||
color: string = "primary"
|
||||
variant: string = "solid"
|
||||
shape: string = "circle"
|
||||
layout: string = "stack"
|
||||
maxVisible: number = 4
|
||||
columns: number = 3
|
||||
borderColor: string = ""
|
||||
showTooltips: boolean = true
|
||||
overflowLabel: string = "Show remaining members"
|
||||
class: string = ""
|
||||
|
||||
}
|
||||
|
||||
state overflowOpen: boolean = false
|
||||
|
||||
functions {
|
||||
shared function visibleMembers() {
|
||||
return items.slice(0, Number(maxVisible))
|
||||
}
|
||||
|
||||
shared function hiddenMembers() {
|
||||
return items.slice(Number(maxVisible))
|
||||
}
|
||||
|
||||
client function toggleOverflow(sourceEvent, root, customEvent) {
|
||||
overflowOpen = !overflowOpen
|
||||
root = sourceEvent.currentTarget.closest("[data-wrn-avatar-group]")
|
||||
|
||||
customEvent = document.createEvent("CustomEvent")
|
||||
customEvent.initCustomEvent("overflow", true, false, {
|
||||
component: "AvatarGroup",
|
||||
open: overflowOpen,
|
||||
hiddenCount: hiddenMembers().length
|
||||
})
|
||||
root.dispatchEvent(customEvent)
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--avatar-group wire-next--variant-{variant} {class}">
|
||||
{#if title}<strong>{title}</strong>{/if}
|
||||
{#if description}<p>{description}</p>{/if}
|
||||
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
|
||||
<slot />
|
||||
</section>
|
||||
<div
|
||||
{...attrs}
|
||||
class="wire-next wire-next--avatar-group {class}"
|
||||
data-wrn-avatar-group
|
||||
data-layout="{layout}"
|
||||
data-shape="{shape}"
|
||||
data-size="{size}"
|
||||
style="--wire-avatar-group-columns:{columns}; --wire-avatar-group-ring:{borderColor || 'var(--wire-color-bg)'}"
|
||||
role="group"
|
||||
aria-label="Avatar group"
|
||||
>
|
||||
<div
|
||||
class="wire-next__avatar-group-members"
|
||||
>
|
||||
{#each visibleMembers() as item}
|
||||
<span
|
||||
class="wire-next__avatar-group-member"
|
||||
data-size="{item.size || size}"
|
||||
data-shape="{item.shape || shape}"
|
||||
data-color="{item.color || color}"
|
||||
data-variant="{item.variant || variant}"
|
||||
tabindex="{showTooltips && (item.tooltip || item.name) ? '0' : '-1'}"
|
||||
aria-label="{item.name || item.alt || item.initials || 'Group member'}"
|
||||
>
|
||||
<span
|
||||
class="wire-next__avatar-group-avatar"
|
||||
>
|
||||
{#if item.src}
|
||||
<img
|
||||
src="{item.src}"
|
||||
alt="{item.alt || item.name || ''}"
|
||||
loading="lazy"
|
||||
/>
|
||||
{:else if item.initials}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
{item.initials}
|
||||
</span>
|
||||
{:else}
|
||||
<span
|
||||
class="icon-[lucide--user-round]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
{#if showTooltips && (item.tooltip || item.name)}
|
||||
<span
|
||||
class="wire-next__avatar-group-tooltip"
|
||||
role="tooltip"
|
||||
>
|
||||
{item.tooltip || item.name}
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
{/each}
|
||||
|
||||
{#if hiddenMembers().length > 0}
|
||||
<span
|
||||
class="wire-next__avatar-group-overflow"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="wire-next__avatar-group-overflow-button"
|
||||
aria-label="{overflowLabel}"
|
||||
aria-expanded="{overflowOpen ? 'true' : 'false'}"
|
||||
@click="toggleOverflow(event)"
|
||||
>
|
||||
+{hiddenMembers().length}
|
||||
</button>
|
||||
|
||||
<span
|
||||
class="wire-next__avatar-group-menu"
|
||||
role="menu"
|
||||
data-show="overflowOpen"
|
||||
aria-hidden="{overflowOpen ? 'false' : 'true'}"
|
||||
>
|
||||
{#each hiddenMembers() as item}
|
||||
<span
|
||||
class="wire-next__avatar-group-menu-item"
|
||||
role="menuitem"
|
||||
>
|
||||
<span
|
||||
class="wire-next__avatar-group-menu-avatar"
|
||||
>
|
||||
{#if item.src}
|
||||
<img
|
||||
src="{item.src}"
|
||||
alt=""
|
||||
loading="lazy"
|
||||
/>
|
||||
{:else}
|
||||
<span>{item.initials || '?'}</span>
|
||||
{/if}
|
||||
</span>
|
||||
<span>{item.name || item.alt || item.initials || 'Team member'}</span>
|
||||
</span>
|
||||
{/each}
|
||||
</span>
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,67 @@
|
||||
component Blockquote {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Blockquote"
|
||||
description = ""
|
||||
items = []
|
||||
variant = "default"
|
||||
class = ""
|
||||
quote: string = "I just wanted to say that I'm very happy with my purchase so far. The documentation is outstanding - clear and detailed."
|
||||
citation: string = ""
|
||||
citationTitle: string = ""
|
||||
citationUrl: string = ""
|
||||
avatarSrc: string = ""
|
||||
avatarAlt: string = ""
|
||||
size: string = "md"
|
||||
color: string = "primary"
|
||||
align: string = "left"
|
||||
variant: string = "default"
|
||||
quoteMark: boolean = true
|
||||
italic: boolean = true
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--blockquote wire-next--variant-{variant} {class}">
|
||||
{#if title}<strong>{title}</strong>{/if}
|
||||
{#if description}<p>{description}</p>{/if}
|
||||
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
|
||||
<slot />
|
||||
</section>
|
||||
<figure
|
||||
{...attrs}
|
||||
class="wire-next wire-next--blockquote wire-next--color-{color} wire-next--size-{size} {class}"
|
||||
data-size="{size}"
|
||||
data-color="{color}"
|
||||
data-align="{align}"
|
||||
data-variant="{variant}"
|
||||
data-italic="{italic}"
|
||||
>
|
||||
<blockquote cite="{citationUrl}">
|
||||
{#if quoteMark}
|
||||
<span class="wire-next__blockquote-mark" aria-hidden="true">“</span>
|
||||
{/if}
|
||||
|
||||
<div class="wire-next__blockquote-copy">
|
||||
{#if quote}
|
||||
<p>{quote}</p>
|
||||
{:else}
|
||||
<slot />
|
||||
{/if}
|
||||
</div>
|
||||
</blockquote>
|
||||
|
||||
{#if citation || citationTitle || avatarSrc}
|
||||
<figcaption class="wire-next__blockquote-citation">
|
||||
{#if avatarSrc}
|
||||
<img
|
||||
class="wire-next__blockquote-avatar"
|
||||
src="{avatarSrc}"
|
||||
alt="{avatarAlt}"
|
||||
loading="lazy"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<span class="wire-next__blockquote-attribution">
|
||||
{#if citation}
|
||||
{#if citationUrl}
|
||||
<cite><a href="{citationUrl}">{citation}</a></cite>
|
||||
{:else}
|
||||
<cite>{citation}</cite>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if citationTitle}<span>{citationTitle}</span>{/if}
|
||||
</span>
|
||||
</figcaption>
|
||||
{/if}
|
||||
</figure>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,323 @@
|
||||
component Breadcrumb {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "Breadcrumb"
|
||||
items = []
|
||||
active = ""
|
||||
orientation = "horizontal"
|
||||
class = ""
|
||||
outputs {
|
||||
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
}
|
||||
|
||||
props {
|
||||
label: string = "Breadcrumb"
|
||||
items: unknown[] = []
|
||||
active: string = ""
|
||||
separator: string = "chevron"
|
||||
showHome: boolean = false
|
||||
homeLabel: string = "Home"
|
||||
homeHref: string = "/"
|
||||
homeIcon: string = "icon-[lucide--house]"
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
variant: string = "minimal"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
view {
|
||||
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--breadcrumb wire-next--{orientation} {class}" aria-label="{label}">
|
||||
{#each items as item}<a href="{item.href}" aria-current="{item.value === active ? 'page' : ''}">{item.label}</a>{/each}
|
||||
<slot />
|
||||
<nav
|
||||
{...attrs}
|
||||
data-ui-component="Breadcrumb"
|
||||
data-size='{size}'
|
||||
data-color='{color}'
|
||||
data-variant='{variant}'
|
||||
aria-label='{label}'
|
||||
class='wire-breadcrumb {class}'
|
||||
>
|
||||
<ol class="wire-breadcrumb__list">
|
||||
{#if showHome}
|
||||
<li class="wire-breadcrumb__item wire-breadcrumb__item--home">
|
||||
<a
|
||||
href='{homeHref || "/"}'
|
||||
class="wire-breadcrumb__link wire-breadcrumb__home"
|
||||
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: { label: homeLabel, href: homeHref || "/", value: "home" }, itemIndex: -1 } }))'
|
||||
>
|
||||
{#if homeIcon === "icon-[lucide--house]"}
|
||||
<span class="icon-[lucide--house] wire-breadcrumb__icon" aria-hidden="true"></span>
|
||||
{:else if homeIcon}
|
||||
<span class='{homeIcon} wire-breadcrumb__icon' aria-hidden="true"></span>
|
||||
{/if}
|
||||
|
||||
<span class="wire-breadcrumb__home-label">{homeLabel}</span>
|
||||
</a>
|
||||
</li>
|
||||
{/if}
|
||||
|
||||
{#each items as item, itemIndex}
|
||||
<li
|
||||
class="wire-breadcrumb__item"
|
||||
data-current='{item.active || item.current || (active && active === (item.value || item.label || item.title)) || (!active && itemIndex === items.length - 1) ? "true" : "false"}'
|
||||
>
|
||||
{#if showHome || itemIndex > 0}
|
||||
<span class="wire-breadcrumb__separator" aria-hidden="true">
|
||||
{#if separator === "slash"}
|
||||
<span>/</span>
|
||||
{:else if separator === "dot"}
|
||||
<span>•</span>
|
||||
{:else if separator === "arrow"}
|
||||
<span>→</span>
|
||||
{:else}
|
||||
<span class="icon-[lucide--chevron-right] wire-breadcrumb__separator-icon"></span>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if item.href && !item.disabled && !(item.active || item.current || (active && active === (item.value || item.label || item.title)) || (!active && itemIndex === items.length - 1))}
|
||||
<a
|
||||
href='{item.href}'
|
||||
target='{item.target || ""}'
|
||||
rel='{item.external ? "noopener noreferrer" : (item.rel || "")}'
|
||||
class="wire-breadcrumb__link"
|
||||
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: item, itemIndex: itemIndex } }))'
|
||||
>
|
||||
{#if item.icon}
|
||||
<span class='{item.icon} wire-breadcrumb__icon' aria-hidden="true"></span>
|
||||
{/if}
|
||||
|
||||
<span class="wire-breadcrumb__label">{item.label || item.title}</span>
|
||||
|
||||
{#if item.external}
|
||||
<span class="icon-[lucide--arrow-up-right] wire-breadcrumb__external" aria-hidden="true"></span>
|
||||
{/if}
|
||||
</a>
|
||||
{:else}
|
||||
<span
|
||||
class="wire-breadcrumb__current"
|
||||
aria-current='{item.active || item.current || (active && active === (item.value || item.label || item.title)) || (!active && itemIndex === items.length - 1) ? "page" : ""}'
|
||||
aria-disabled='{item.disabled ? "true" : "false"}'
|
||||
>
|
||||
{#if item.icon}
|
||||
<span class='{item.icon} wire-breadcrumb__icon' aria-hidden="true"></span>
|
||||
{/if}
|
||||
|
||||
<span class="wire-breadcrumb__label">{item.label || item.title}</span>
|
||||
</span>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
</nav>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-breadcrumb {
|
||||
--breadcrumb-accent: var(--wire-color-primary);
|
||||
--breadcrumb-soft: var(--wire-color-primary-soft);
|
||||
--breadcrumb-muted: var(--wire-color-primary-muted);
|
||||
--breadcrumb-text: var(--wire-color-primary-text);
|
||||
|
||||
min-width: 0;
|
||||
color: var(--wire-color-text-muted);
|
||||
}
|
||||
|
||||
.wire-breadcrumb[data-color="secondary"] {
|
||||
--breadcrumb-accent: var(--wire-color-secondary);
|
||||
--breadcrumb-soft: var(--wire-color-secondary-soft);
|
||||
--breadcrumb-muted: var(--wire-color-secondary-muted);
|
||||
--breadcrumb-text: var(--wire-color-secondary-text);
|
||||
}
|
||||
|
||||
.wire-breadcrumb[data-color="info"] {
|
||||
--breadcrumb-accent: var(--wire-color-info);
|
||||
--breadcrumb-soft: var(--wire-color-info-soft);
|
||||
--breadcrumb-muted: var(--wire-color-info-muted);
|
||||
--breadcrumb-text: var(--wire-color-info-text);
|
||||
}
|
||||
|
||||
.wire-breadcrumb[data-color="success"] {
|
||||
--breadcrumb-accent: var(--wire-color-success);
|
||||
--breadcrumb-soft: var(--wire-color-success-soft);
|
||||
--breadcrumb-muted: var(--wire-color-success-muted);
|
||||
--breadcrumb-text: var(--wire-color-success-text);
|
||||
}
|
||||
|
||||
.wire-breadcrumb[data-color="warning"] {
|
||||
--breadcrumb-accent: var(--wire-color-warning);
|
||||
--breadcrumb-soft: var(--wire-color-warning-soft);
|
||||
--breadcrumb-muted: var(--wire-color-warning-muted);
|
||||
--breadcrumb-text: var(--wire-color-warning-text);
|
||||
}
|
||||
|
||||
.wire-breadcrumb[data-color="danger"] {
|
||||
--breadcrumb-accent: var(--wire-color-danger);
|
||||
--breadcrumb-soft: var(--wire-color-danger-soft);
|
||||
--breadcrumb-muted: var(--wire-color-danger-muted);
|
||||
--breadcrumb-text: var(--wire-color-danger-text);
|
||||
}
|
||||
|
||||
.wire-breadcrumb[data-variant="soft"] {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
padding: 0.45rem 0.65rem;
|
||||
background: var(--breadcrumb-soft);
|
||||
border: 1px solid var(--breadcrumb-muted);
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.wire-breadcrumb[data-variant="outline"] {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
padding: 0.45rem 0.65rem;
|
||||
background: var(--wire-color-surface-raised);
|
||||
border: 1px solid var(--wire-color-border);
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: var(--wire-shadow-sm);
|
||||
}
|
||||
|
||||
.wire-breadcrumb[data-variant="contrast"] {
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
}
|
||||
|
||||
.wire-breadcrumb__list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-x: auto;
|
||||
list-style: none;
|
||||
scrollbar-width: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__list::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__separator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-inline: 0.45rem;
|
||||
color: var(--wire-color-text-subtle);
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__separator-icon {
|
||||
width: 0.9rem;
|
||||
height: 0.9rem;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__link,
|
||||
.wire-breadcrumb__current {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
min-width: 0;
|
||||
min-height: 2rem;
|
||||
padding: 0.25rem 0.4rem;
|
||||
border-radius: 0.55rem;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.25rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.wire-breadcrumb[data-size="sm"] .wire-breadcrumb__link,
|
||||
.wire-breadcrumb[data-size="sm"] .wire-breadcrumb__current {
|
||||
min-height: 1.75rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.wire-breadcrumb[data-size="lg"] .wire-breadcrumb__link,
|
||||
.wire-breadcrumb[data-size="lg"] .wire-breadcrumb__current {
|
||||
min-height: 2.25rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__link {
|
||||
color: var(--wire-color-text-muted);
|
||||
font-weight: 600;
|
||||
transition:
|
||||
color 160ms ease,
|
||||
background 160ms ease;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__link:hover {
|
||||
color: var(--breadcrumb-accent);
|
||||
background: var(--breadcrumb-soft);
|
||||
}
|
||||
|
||||
.wire-breadcrumb__link:focus-visible {
|
||||
color: var(--breadcrumb-accent);
|
||||
outline: 2px solid var(--breadcrumb-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__current {
|
||||
max-width: 22rem;
|
||||
color: var(--wire-color-text);
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__item[data-current="true"] .wire-breadcrumb__current {
|
||||
color: var(--breadcrumb-text);
|
||||
background: var(--breadcrumb-soft);
|
||||
}
|
||||
|
||||
.wire-breadcrumb__current[aria-disabled="true"] {
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__icon,
|
||||
.wire-breadcrumb__external {
|
||||
flex: 0 0 auto;
|
||||
width: 0.95rem;
|
||||
height: 0.95rem;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__home {
|
||||
color: var(--breadcrumb-accent);
|
||||
}
|
||||
|
||||
.wire-breadcrumb__label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__link,
|
||||
.wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__current,
|
||||
.wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__home {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__link:hover,
|
||||
.wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__item[data-current="true"] .wire-breadcrumb__current {
|
||||
color: #ffffff;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.wire-breadcrumb[data-variant="contrast"] .wire-breadcrumb__separator {
|
||||
color: rgba(255, 255, 255, 0.62);
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.wire-breadcrumb__home-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wire-breadcrumb__current {
|
||||
max-width: 13rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wire-breadcrumb__link {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,92 @@
|
||||
component ButtonGroup {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Button Group"
|
||||
description = ""
|
||||
items = []
|
||||
variant = "default"
|
||||
class = ""
|
||||
outputs {
|
||||
click(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
select(payload: { value: string | number | boolean | null | object; previousValue: string | number | boolean | null | object; item: string | number | boolean | null | object; index: number })
|
||||
change(payload: { value: string | number | boolean | null | object; previousValue: string | number | boolean | null | object; item: string | number | boolean | null | object; index: number })
|
||||
}
|
||||
|
||||
props {
|
||||
items: unknown[] = []
|
||||
value: string = ""
|
||||
size: string = "md"
|
||||
color: string = "primary"
|
||||
variant: string = "default"
|
||||
orientation: string = "horizontal"
|
||||
responsive: boolean = false
|
||||
attached: boolean = true
|
||||
selectable: boolean = false
|
||||
toolbar: boolean = false
|
||||
disabled: boolean = false
|
||||
ariaLabel: string = "Button group"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
state selectedValue = value
|
||||
|
||||
functions {
|
||||
shared function itemValue(item, index) {
|
||||
return item.value || item.label || String(index)
|
||||
}
|
||||
|
||||
client function selectItem(item, index) {
|
||||
previousValue = selectedValue
|
||||
nextValue = itemValue(item, index)
|
||||
|
||||
output.select({
|
||||
value: nextValue,
|
||||
previousValue: previousValue,
|
||||
item: item,
|
||||
index: index
|
||||
})
|
||||
|
||||
if (selectable && previousValue !== nextValue) {
|
||||
selectedValue = nextValue
|
||||
output.change({
|
||||
value: nextValue,
|
||||
previousValue: previousValue,
|
||||
item: item,
|
||||
index: index
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--button-group wire-next--variant-{variant} {class}">
|
||||
{#if title}<strong>{title}</strong>{/if}
|
||||
{#if description}<p>{description}</p>{/if}
|
||||
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
|
||||
<div
|
||||
{...attrs}
|
||||
class="wire-next wire-next--button-group {class}"
|
||||
role="{toolbar ? 'toolbar' : 'group'}"
|
||||
aria-label="{ariaLabel}"
|
||||
aria-orientation="{orientation}"
|
||||
data-size="{size}"
|
||||
data-color="{color}"
|
||||
data-variant="{variant}"
|
||||
data-orientation="{orientation}"
|
||||
data-responsive="{responsive}"
|
||||
data-attached="{attached}"
|
||||
data-selectable="{selectable}"
|
||||
data-disabled="{disabled}"
|
||||
>
|
||||
{#if items}
|
||||
{#each items as item, index}
|
||||
<button
|
||||
type="{item.type || 'button'}"
|
||||
class="wire-btn wire-btn--variant-{item.variant || variant} wire-btn--color-{item.color || color} wire-btn--size-{item.size || size}"
|
||||
disabled="{disabled || item.disabled}"
|
||||
aria-label="{item.ariaLabel || item.label}"
|
||||
aria-pressed="{selectable ? (selectedValue === (item.value || item.label || String(index)) ? 'true' : 'false') : ''}"
|
||||
data-value="{item.value || item.label || String(index)}"
|
||||
data-selected="{selectable && selectedValue === (item.value || item.label || String(index)) ? 'true' : 'false'}"
|
||||
@click="selectItem(item, index)"
|
||||
>
|
||||
{#if item.icon}
|
||||
<span class="wire-btn__icon {item.icon}" aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span class="wire-btn__label">{item.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
<slot />
|
||||
</section>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,464 @@
|
||||
// Interactive, hydration-safe content carousel.
|
||||
component Carousel {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Carousel"
|
||||
description = ""
|
||||
items = []
|
||||
variant = "default"
|
||||
class = ""
|
||||
outputs {
|
||||
initialize(payload: { index: number; count: number })
|
||||
change(payload: { index: number; previousIndex: number; item: string | number | boolean | null | object; reason: string | boolean })
|
||||
previous(payload: { index: number; previousIndex: number; item: string | number | boolean | null | object })
|
||||
next(payload: { index: number; previousIndex: number; item: string | number | boolean | null | object })
|
||||
play(payload: { index: number; interval: number })
|
||||
pause(payload: { index: number })
|
||||
reachStart(payload: { index: number })
|
||||
reachEnd(payload: { index: number })
|
||||
dragStart(payload: { index: number; x: null })
|
||||
dragEnd(payload: { index: number; distance: number })
|
||||
}
|
||||
|
||||
props {
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
title: string = ""
|
||||
description: string = ""
|
||||
items: unknown[] = []
|
||||
activeIndex: number = 0
|
||||
slidesPerView: number = 1
|
||||
gap: string = "0.75rem"
|
||||
showPagination: boolean = false
|
||||
isAutoPlay: boolean = false
|
||||
autoplayInterval: number = 4000
|
||||
isInfiniteLoop: boolean = false
|
||||
isRTL: boolean = false
|
||||
isCentered: boolean = false
|
||||
isDraggable: boolean = false
|
||||
isAutoHeight: boolean = false
|
||||
isSnap: boolean = false
|
||||
showCounter: boolean = false
|
||||
thumbnails: string = "none"
|
||||
ariaLabel: string = "Content carousel"
|
||||
variant: string = "default"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
state currentIndex = activeIndex
|
||||
state playing: boolean = false
|
||||
state dragOrigin = null
|
||||
state dragOffset: number = 0
|
||||
state autoplayTimer = null
|
||||
state carouselRoot = null
|
||||
|
||||
functions {
|
||||
shared function slideCount() {
|
||||
return items.length
|
||||
}
|
||||
|
||||
shared function maximumIndex(count, visible) {
|
||||
count = slideCount()
|
||||
visible = Math.max(1, Number(slidesPerView) || 1)
|
||||
if (isCentered || isSnap) {
|
||||
return Math.max(0, count - 1)
|
||||
}
|
||||
return Math.max(0, count - visible)
|
||||
}
|
||||
|
||||
shared function normalizedIndex(index, maximum) {
|
||||
maximum = maximumIndex()
|
||||
if (slideCount() === 0) {
|
||||
return 0
|
||||
}
|
||||
if (isInfiniteLoop) {
|
||||
if (index < 0) {
|
||||
return maximum
|
||||
}
|
||||
if (index > maximum) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
return Math.max(0, Math.min(index, maximum))
|
||||
}
|
||||
|
||||
client function rememberRoot(sourceEvent, root) {
|
||||
if (!sourceEvent || !sourceEvent.currentTarget) {
|
||||
return
|
||||
}
|
||||
root = sourceEvent.currentTarget.closest(".wire-next--carousel")
|
||||
if (root) {
|
||||
carouselRoot = root
|
||||
}
|
||||
}
|
||||
|
||||
client function syncNavigation(sourceEvent, index, root, viewport, slides, slide, thumbnails, thumbnail, rail) {
|
||||
rememberRoot(sourceEvent)
|
||||
root = carouselRoot
|
||||
if (!root) {
|
||||
return
|
||||
}
|
||||
if (isSnap) {
|
||||
viewport = root.querySelector(".wire-next__carousel-viewport")
|
||||
slides = root.querySelectorAll(".wire-next__carousel-slide")
|
||||
slide = slides[index]
|
||||
if (viewport && slide) {
|
||||
viewport.scrollTo({
|
||||
left: slide.offsetLeft - (viewport.clientWidth - slide.clientWidth) / 2,
|
||||
behavior: "smooth"
|
||||
})
|
||||
}
|
||||
}
|
||||
thumbnails = root.querySelectorAll(".wire-next__carousel-thumbnails button")
|
||||
if (thumbnails[index]) {
|
||||
thumbnail = thumbnails[index]
|
||||
rail = thumbnail.parentElement
|
||||
rail.scrollTo({
|
||||
left: thumbnail.offsetLeft - (rail.clientWidth - thumbnail.clientWidth) / 2,
|
||||
top: thumbnail.offsetTop - (rail.clientHeight - thumbnail.clientHeight) / 2,
|
||||
behavior: "smooth"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
client function handleSnapScroll(sourceEvent, slides, viewport, viewportCenter, closestIndex, closestDistance, slide, slideCenter, distance, previousIndex) {
|
||||
if (!isSnap) {
|
||||
return
|
||||
}
|
||||
rememberRoot(sourceEvent)
|
||||
viewport = sourceEvent.currentTarget
|
||||
slides = viewport.querySelectorAll(".wire-next__carousel-slide")
|
||||
viewportCenter = viewport.scrollLeft + viewport.clientWidth / 2
|
||||
closestIndex = currentIndex
|
||||
closestDistance = 1000000000
|
||||
slides.forEach(function (candidate, index) {
|
||||
slideCenter = candidate.offsetLeft + candidate.clientWidth / 2
|
||||
distance = Math.abs(slideCenter - viewportCenter)
|
||||
if (distance < closestDistance) {
|
||||
closestDistance = distance
|
||||
closestIndex = index
|
||||
}
|
||||
})
|
||||
closestIndex = normalizedIndex(closestIndex)
|
||||
if (closestIndex !== currentIndex) {
|
||||
previousIndex = currentIndex
|
||||
currentIndex = closestIndex
|
||||
output.change({
|
||||
index: currentIndex,
|
||||
previousIndex: previousIndex,
|
||||
item: items[currentIndex],
|
||||
reason: "snap"
|
||||
})
|
||||
slides = carouselRoot.querySelectorAll(".wire-next__carousel-thumbnails button")
|
||||
slide = slides[currentIndex]
|
||||
if (slide) {
|
||||
viewport = slide.parentElement
|
||||
viewport.scrollTo({
|
||||
left: slide.offsetLeft - (viewport.clientWidth - slide.clientWidth) / 2,
|
||||
top: slide.offsetTop - (viewport.clientHeight - slide.clientHeight) / 2,
|
||||
behavior: "smooth"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client function selectSlide(index, reason, sourceEvent, previousIndex) {
|
||||
if (slideCount() === 0) {
|
||||
return
|
||||
}
|
||||
previousIndex = currentIndex
|
||||
currentIndex = normalizedIndex(Number(index))
|
||||
syncNavigation(sourceEvent, currentIndex)
|
||||
if (previousIndex === currentIndex && reason !== "initialize") {
|
||||
return
|
||||
}
|
||||
output.change({
|
||||
index: currentIndex,
|
||||
previousIndex: previousIndex,
|
||||
item: items[currentIndex],
|
||||
reason: reason || "select"
|
||||
})
|
||||
if (currentIndex === 0) {
|
||||
output.reachStart({ index: currentIndex })
|
||||
}
|
||||
if (currentIndex === maximumIndex()) {
|
||||
output.reachEnd({ index: currentIndex })
|
||||
}
|
||||
}
|
||||
|
||||
client function previousSlide(sourceEvent, previousIndex) {
|
||||
previousIndex = currentIndex
|
||||
selectSlide(currentIndex - 1, "previous", sourceEvent)
|
||||
output.previous({
|
||||
index: currentIndex,
|
||||
previousIndex: previousIndex,
|
||||
item: items[currentIndex]
|
||||
})
|
||||
}
|
||||
|
||||
client function nextSlide(sourceEvent, previousIndex) {
|
||||
previousIndex = currentIndex
|
||||
selectSlide(currentIndex + 1, "next", sourceEvent)
|
||||
output.next({
|
||||
index: currentIndex,
|
||||
previousIndex: previousIndex,
|
||||
item: items[currentIndex]
|
||||
})
|
||||
}
|
||||
|
||||
client function handleKeydown(sourceEvent) {
|
||||
if (sourceEvent.key === "ArrowLeft") {
|
||||
sourceEvent.preventDefault()
|
||||
if (isRTL) {
|
||||
nextSlide(sourceEvent)
|
||||
} else {
|
||||
previousSlide(sourceEvent)
|
||||
}
|
||||
}
|
||||
if (sourceEvent.key === "ArrowRight") {
|
||||
sourceEvent.preventDefault()
|
||||
if (isRTL) {
|
||||
previousSlide(sourceEvent)
|
||||
} else {
|
||||
nextSlide(sourceEvent)
|
||||
}
|
||||
}
|
||||
if (sourceEvent.key === "Home") {
|
||||
sourceEvent.preventDefault()
|
||||
selectSlide(0, "keyboard", sourceEvent)
|
||||
}
|
||||
if (sourceEvent.key === "End") {
|
||||
sourceEvent.preventDefault()
|
||||
selectSlide(maximumIndex(), "keyboard", sourceEvent)
|
||||
}
|
||||
}
|
||||
|
||||
client function beginDrag(sourceEvent) {
|
||||
if (!isDraggable || isSnap) {
|
||||
return
|
||||
}
|
||||
sourceEvent.preventDefault()
|
||||
dragOrigin = sourceEvent.clientX
|
||||
dragOffset = 0
|
||||
sourceEvent.currentTarget.setPointerCapture(sourceEvent.pointerId)
|
||||
output.dragStart({ index: currentIndex, x: dragOrigin })
|
||||
}
|
||||
|
||||
client function moveDrag(sourceEvent) {
|
||||
if (!isDraggable || isSnap || dragOrigin === null) {
|
||||
return
|
||||
}
|
||||
sourceEvent.preventDefault()
|
||||
dragOffset = sourceEvent.clientX - dragOrigin
|
||||
}
|
||||
|
||||
shared function cancelDrag() {
|
||||
dragOrigin = null
|
||||
dragOffset = 0
|
||||
}
|
||||
|
||||
client function endDrag(sourceEvent, distance) {
|
||||
if (!isDraggable || isSnap || dragOrigin === null) {
|
||||
return
|
||||
}
|
||||
sourceEvent.preventDefault()
|
||||
distance = dragOffset || sourceEvent.clientX - dragOrigin
|
||||
dragOrigin = null
|
||||
dragOffset = 0
|
||||
if (Math.abs(distance) > 20) {
|
||||
if ((distance < 0 && !isRTL) || (distance > 0 && isRTL)) {
|
||||
nextSlide(sourceEvent)
|
||||
} else {
|
||||
previousSlide(sourceEvent)
|
||||
}
|
||||
}
|
||||
output.dragEnd({
|
||||
index: currentIndex,
|
||||
distance: distance
|
||||
})
|
||||
}
|
||||
|
||||
shared function advanceAutoplay() {
|
||||
if (currentIndex >= maximumIndex()) {
|
||||
selectSlide(0, "autoplay")
|
||||
} else {
|
||||
selectSlide(currentIndex + 1, "autoplay")
|
||||
}
|
||||
}
|
||||
|
||||
client function startAutoplay() {
|
||||
if (!isAutoPlay || playing || slideCount() < 2) {
|
||||
return
|
||||
}
|
||||
playing = true
|
||||
autoplayTimer = setInterval(advanceAutoplay, Math.max(1000, Number(autoplayInterval)))
|
||||
output.play({ index: currentIndex, interval: autoplayInterval })
|
||||
}
|
||||
|
||||
client function pauseAutoplay() {
|
||||
if (autoplayTimer) {
|
||||
clearInterval(autoplayTimer)
|
||||
}
|
||||
autoplayTimer = null
|
||||
if (playing) {
|
||||
output.pause({ index: currentIndex })
|
||||
}
|
||||
playing = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
lifecycle {
|
||||
mount {
|
||||
output.initialize({ index: currentIndex, count: slideCount() })
|
||||
startAutoplay()
|
||||
}
|
||||
unmount {
|
||||
if (autoplayTimer) {
|
||||
clearInterval(autoplayTimer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--carousel wire-next--variant-{variant} {class}">
|
||||
{#if title}<strong>{title}</strong>{/if}
|
||||
{#if description}<p>{description}</p>{/if}
|
||||
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
|
||||
<slot />
|
||||
<section
|
||||
{...attrs}
|
||||
class="wire-next wire-next--carousel wire-next--color-{color} wire-next--size-{size} wire-next--variant-{variant} {class}"
|
||||
data-rtl="{isRTL}"
|
||||
data-centered="{isCentered}"
|
||||
data-draggable="{isDraggable && !isSnap}"
|
||||
data-dragging="{dragOrigin !== null}"
|
||||
data-auto-height="{isAutoHeight}"
|
||||
data-snap="{isSnap}"
|
||||
data-thumbnails="{thumbnails}"
|
||||
dir="{isRTL ? 'rtl' : 'ltr'}"
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
aria-label="{ariaLabel}"
|
||||
@keydown="handleKeydown(event)"
|
||||
@mouseenter="rememberRoot(event); pauseAutoplay()"
|
||||
@mouseleave="startAutoplay()"
|
||||
@focusin="rememberRoot(event); pauseAutoplay()"
|
||||
@focusout="startAutoplay()"
|
||||
>
|
||||
{#if title || description}
|
||||
<header class="wire-next__carousel-header">
|
||||
{#if title}<h3>{title}</h3>{/if}
|
||||
{#if description}<p>{description}</p>{/if}
|
||||
</header>
|
||||
{/if}
|
||||
|
||||
<div class="wire-next__carousel-layout">
|
||||
{#if thumbnails === "vertical"}
|
||||
<div class="wire-next__carousel-thumbnails" aria-label="Choose a slide">
|
||||
{#each items as item, index}
|
||||
<button
|
||||
type="button"
|
||||
data-active="{index === currentIndex}"
|
||||
aria-label="Show slide {index + 1}: {item.label || item.title}"
|
||||
aria-current="{index === currentIndex ? 'true' : 'false'}"
|
||||
@click="selectSlide(index, 'thumbnail', event)"
|
||||
>
|
||||
{#if item.thumbnail}<img src="{item.thumbnail}" alt="" />{/if}
|
||||
<span>{item.label || item.title || "Slide " + (index + 1)}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="wire-next__carousel-main">
|
||||
<div class="wire-next__carousel-stage">
|
||||
<div
|
||||
class="wire-next__carousel-viewport"
|
||||
tabindex="0"
|
||||
@scroll="handleSnapScroll(event)"
|
||||
@pointerdown="beginDrag(event)"
|
||||
@pointermove="moveDrag(event)"
|
||||
@pointerup="endDrag(event)"
|
||||
@pointercancel="cancelDrag()"
|
||||
@lostpointercapture="cancelDrag()"
|
||||
>
|
||||
<div
|
||||
class="wire-next__carousel-track"
|
||||
style="--wire-carousel-index: {currentIndex}; --wire-carousel-per-view: {slidesPerView}; --wire-carousel-gap: {gap}; --wire-carousel-drag-offset: {dragOffset}px"
|
||||
>
|
||||
{#each items as item, index}
|
||||
<article
|
||||
class="wire-next__carousel-slide"
|
||||
data-active="{index === currentIndex}"
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
aria-label="{index + 1} of {items.length}"
|
||||
aria-hidden="{index === currentIndex ? 'false' : 'true'}"
|
||||
>
|
||||
{#if item.imageSrc}
|
||||
<img src="{item.imageSrc}" alt="{item.imageAlt || item.title || ''}" />
|
||||
{/if}
|
||||
<div class="wire-next__carousel-slide-content">
|
||||
{#if item.eyebrow}<span>{item.eyebrow}</span>{/if}
|
||||
{#if item.title || item.label}<h4>{item.title || item.label}</h4>{/if}
|
||||
{#if item.description}<p>{item.description}</p>{/if}
|
||||
{#if item.actionLabel}
|
||||
<a href="{item.actionHref || '#'}">{item.actionLabel}</a>
|
||||
{/if}
|
||||
</div>
|
||||
</article>
|
||||
{/each}
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="wire-next__carousel-control wire-next__carousel-control--previous"
|
||||
type="button"
|
||||
aria-label="Previous slide"
|
||||
disabled="{!isInfiniteLoop && currentIndex === 0}"
|
||||
@click="previousSlide(event)"
|
||||
>
|
||||
<span class="icon-[lucide--chevron-left]" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button
|
||||
class="wire-next__carousel-control wire-next__carousel-control--next"
|
||||
type="button"
|
||||
aria-label="Next slide"
|
||||
disabled="{!isInfiniteLoop && currentIndex === maximumIndex()}"
|
||||
@click="nextSlide(event)"
|
||||
>
|
||||
<span class="icon-[lucide--chevron-right]" aria-hidden="true"></span>
|
||||
</button>
|
||||
|
||||
{#if showCounter}
|
||||
<output class="wire-next__carousel-counter" aria-live="polite">
|
||||
{currentIndex + 1} / {items.length}
|
||||
</output>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if showPagination}
|
||||
<div class="wire-next__carousel-pagination" aria-label="Choose a slide">
|
||||
{#each items as item, index}
|
||||
<button
|
||||
type="button"
|
||||
data-active="{index === currentIndex}"
|
||||
aria-label="Show slide {index + 1}"
|
||||
aria-current="{index === currentIndex ? 'true' : 'false'}"
|
||||
@click="selectSlide(index, 'pagination', event)"
|
||||
></button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if thumbnails === "horizontal"}
|
||||
<div class="wire-next__carousel-thumbnails" aria-label="Choose a slide">
|
||||
{#each items as item, index}
|
||||
<button
|
||||
type="button"
|
||||
data-active="{index === currentIndex}"
|
||||
aria-label="Show slide {index + 1}: {item.label || item.title}"
|
||||
aria-current="{index === currentIndex ? 'true' : 'false'}"
|
||||
@click="selectSlide(index, 'thumbnail', event)"
|
||||
>
|
||||
{#if item.thumbnail}<img src="{item.thumbnail}" alt="" />{/if}
|
||||
<span>{item.label || item.title || "Slide " + (index + 1)}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
component Chart {
|
||||
outputs {
|
||||
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
dataPointClick(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
|
||||
legendToggle(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Chart"
|
||||
description = ""
|
||||
items = []
|
||||
variant = "default"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
title: string = "Chart"
|
||||
description: string = ""
|
||||
items: unknown[] = []
|
||||
variant: string = "default"
|
||||
class: string = ""
|
||||
}
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--chart wire-next--variant-{variant} {class}">
|
||||
|
||||
@@ -1,18 +1,149 @@
|
||||
component ChatBubble {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Chat Bubble"
|
||||
description = ""
|
||||
items = []
|
||||
variant = "default"
|
||||
class = ""
|
||||
outputs {
|
||||
action(payload: { action: boolean; item: string | number | boolean | null | object; index: number })
|
||||
messageClick(payload: { item: string | number | boolean | null | object; index: number; direction: string })
|
||||
avatarClick(payload: { item: string | number | boolean | null | object; index: number; direction: string })
|
||||
linkClick(payload: { link: string | number | boolean | null | object; item: string | number | boolean | null | object; index: number })
|
||||
}
|
||||
|
||||
props {
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
title: string = ""
|
||||
description: string = ""
|
||||
items: unknown[] = []
|
||||
oneSided: boolean = false
|
||||
showAvatars: boolean = false
|
||||
showMetadata: boolean = false
|
||||
ariaLabel: string = "Conversation"
|
||||
variant: string = "default"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
functions {
|
||||
shared function messageDirection(item) {
|
||||
return item.direction === "outgoing" ? "outgoing" : "incoming"
|
||||
}
|
||||
|
||||
client function selectMessage(item, index) {
|
||||
output.messageClick({
|
||||
item: item,
|
||||
index: index,
|
||||
direction: messageDirection(item)
|
||||
})
|
||||
}
|
||||
|
||||
client function selectAvatar(sourceEvent, item, index) {
|
||||
sourceEvent.stopPropagation()
|
||||
output.avatarClick({
|
||||
item: item,
|
||||
index: index,
|
||||
direction: messageDirection(item)
|
||||
})
|
||||
}
|
||||
|
||||
client function selectLink(sourceEvent, link, item, index) {
|
||||
sourceEvent.stopPropagation()
|
||||
output.linkClick({
|
||||
link: link,
|
||||
item: item,
|
||||
index: index
|
||||
})
|
||||
}
|
||||
|
||||
client function selectAction(sourceEvent, item, index) {
|
||||
sourceEvent.stopPropagation()
|
||||
output.action({
|
||||
action: item.action || "retry",
|
||||
item: item,
|
||||
index: index
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--chat-bubble wire-next--variant-{variant} {class}">
|
||||
{#if title}<strong>{title}</strong>{/if}
|
||||
{#if description}<p>{description}</p>{/if}
|
||||
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
|
||||
<section
|
||||
{...attrs}
|
||||
class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--chat-bubble wire-next--variant-{variant} {class}"
|
||||
data-one-sided="{oneSided}"
|
||||
role="log"
|
||||
aria-label="{ariaLabel}"
|
||||
aria-live="polite"
|
||||
>
|
||||
{#if title || description}
|
||||
<header class="wire-next__chat-header">
|
||||
{#if title}<h3>{title}</h3>{/if}
|
||||
{#if description}<p>{description}</p>{/if}
|
||||
</header>
|
||||
{/if}
|
||||
|
||||
{#if items.length}
|
||||
<div class="wire-next__chat-thread">
|
||||
{#each items as item, index}
|
||||
<article
|
||||
class="wire-next__chat-message"
|
||||
data-direction="{messageDirection(item)}"
|
||||
aria-label="{messageDirection(item) === 'outgoing' ? 'Sent message' : 'Received message'}"
|
||||
>
|
||||
<div class="wire-next__chat-row">
|
||||
{#if showAvatars && (item.avatarSrc || item.avatarFallback)}
|
||||
<button
|
||||
class="wire-next__chat-avatar"
|
||||
type="button"
|
||||
aria-label="{item.avatarLabel || item.author || 'Message author'}"
|
||||
@click="selectAvatar(event, item, index)"
|
||||
>
|
||||
{#if item.avatarSrc}
|
||||
<img src="{item.avatarSrc}" alt="{item.avatarAlt || ''}" />
|
||||
{:else}
|
||||
<span>{item.avatarFallback}</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="wire-next__chat-content"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="selectMessage(item, index)"
|
||||
@keydown="if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); selectMessage(item, index) }"
|
||||
>
|
||||
{#if item.title}<h4>{item.title}</h4>{/if}
|
||||
{#if item.text}<p>{item.text}</p>{/if}
|
||||
{#if item.bullets && item.bullets.length}
|
||||
<ul>
|
||||
{#each item.bullets as bullet}<li>{bullet}</li>{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{#if item.links && item.links.length}
|
||||
<nav aria-label="Message links">
|
||||
{#each item.links as link}
|
||||
<a
|
||||
href="{link.href || '#'}"
|
||||
@click="selectLink(event, link, item, index)"
|
||||
>{link.label}</a>
|
||||
{/each}
|
||||
</nav>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showMetadata && (item.timestamp || item.status || item.actionLabel)}
|
||||
<footer class="wire-next__chat-meta" data-tone="{item.statusTone || 'muted'}">
|
||||
{#if item.statusIcon}<span class="{item.statusIcon}" aria-hidden="true"></span>{/if}
|
||||
{#if item.status}<span>{item.status}</span>{/if}
|
||||
{#if item.timestamp}<time datetime="{item.datetime || ''}">{item.timestamp}</time>{/if}
|
||||
{#if item.actionLabel}
|
||||
<button type="button" @click="selectAction(event, item, index)">
|
||||
{item.actionLabel}
|
||||
</button>
|
||||
{/if}
|
||||
</footer>
|
||||
{/if}
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<slot />
|
||||
</section>
|
||||
}
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
component Clipboard {
|
||||
outputs {
|
||||
copy(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
|
||||
success(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
error(payload: { error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Clipboard"
|
||||
description = ""
|
||||
items = []
|
||||
variant = "default"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
title: string = "Clipboard"
|
||||
description: string = ""
|
||||
items: unknown[] = []
|
||||
variant: string = "default"
|
||||
class: string = ""
|
||||
}
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--clipboard wire-next--variant-{variant} {class}">
|
||||
|
||||
@@ -1,15 +1,98 @@
|
||||
component Collapse {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
items = []
|
||||
multiple = false
|
||||
class = ""
|
||||
outputs {
|
||||
toggle(payload: { index: number; item: string | number | boolean | null | object; open: boolean; openIndexes: number[] })
|
||||
open(payload: { index: number; item: string | number | boolean | null | object })
|
||||
close(payload: { index: number; item: string | number | boolean | null | object })
|
||||
}
|
||||
|
||||
props {
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
items: unknown[] = []
|
||||
multiple: boolean = false
|
||||
mode: string = "panel"
|
||||
initialOpenIndexes: unknown[] = []
|
||||
ariaLabel: string = "Collapsible content"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
state openIndexes = initialOpenIndexes
|
||||
|
||||
functions {
|
||||
shared function isOpen(index) {
|
||||
return openIndexes.includes(index)
|
||||
}
|
||||
|
||||
client function toggleItem(index, item, opening) {
|
||||
if (item.disabled) {
|
||||
return
|
||||
}
|
||||
opening = !isOpen(index)
|
||||
if (opening) {
|
||||
if (multiple) {
|
||||
openIndexes = openIndexes.concat(index)
|
||||
} else {
|
||||
openIndexes = [index]
|
||||
}
|
||||
output.open({ index: index, item: item })
|
||||
} else {
|
||||
openIndexes = openIndexes.filter((openIndex) => openIndex !== index)
|
||||
output.close({ index: index, item: item })
|
||||
}
|
||||
output.toggle({
|
||||
index: index,
|
||||
item: item,
|
||||
open: opening,
|
||||
openIndexes: openIndexes
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--collapse {class}">
|
||||
{#each items as item}<details open="{item.open}"><summary>{item.label}</summary><div>{item.content}</div></details>{/each}
|
||||
<section
|
||||
{...attrs}
|
||||
class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--collapse {class}"
|
||||
data-mode="{mode}"
|
||||
aria-label="{ariaLabel}"
|
||||
>
|
||||
{#each items as item, index}
|
||||
<article class="wire-next__collapse-item" data-open="{isOpen(index)}">
|
||||
{#if mode === "inline" && item.preview}
|
||||
<p class="wire-next__collapse-preview">{item.preview}</p>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="wire-next__collapse-trigger"
|
||||
type="button"
|
||||
aria-expanded="{isOpen(index) ? 'true' : 'false'}"
|
||||
aria-controls="{item.id || 'collapse-panel-' + index}"
|
||||
disabled="{item.disabled}"
|
||||
@click="toggleItem(index, item)"
|
||||
>
|
||||
<span>{isOpen(index) ? (item.closeLabel || "Read less") : item.label}</span>
|
||||
<span class="icon-[lucide--chevron-down] wire-next__collapse-chevron" aria-hidden="true"></span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
id="{item.id || 'collapse-panel-' + index}"
|
||||
class="wire-next__collapse-panel"
|
||||
data-open="{isOpen(index)}"
|
||||
aria-hidden="{isOpen(index) ? 'false' : 'true'}"
|
||||
inert="{!isOpen(index)}"
|
||||
>
|
||||
<div class="wire-next__collapse-content">
|
||||
{#if item.title}<h4>{item.title}</h4>{/if}
|
||||
{#if item.content}<p>{item.content}</p>{/if}
|
||||
{#if item.links && item.links.length}
|
||||
<nav aria-label="Related links">
|
||||
{#each item.links as link}<a href="{link.href || '#'}">{link.label}</a>{/each}
|
||||
</nav>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{/each}
|
||||
<slot />
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,41 @@
|
||||
component ColorPicker {
|
||||
outputs {
|
||||
input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "Color Picker"
|
||||
name = ""
|
||||
value = ""
|
||||
placeholder = ""
|
||||
type = "color"
|
||||
min = ""
|
||||
max = ""
|
||||
step = ""
|
||||
disabled = false
|
||||
required = false
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
id: string = ""
|
||||
name: string = ""
|
||||
label: string = "Color"
|
||||
hiddenLabel: boolean = false
|
||||
placeholder: string = ""
|
||||
value: string = "#2563eb"
|
||||
icon: string = ""
|
||||
iconPosition: string = "start"
|
||||
helperText: string = ""
|
||||
cornerHint: string = ""
|
||||
error: string = ""
|
||||
inline: boolean = false
|
||||
variant: string = "normal"
|
||||
readonly: boolean = false
|
||||
disabled: boolean = false
|
||||
required: boolean = false
|
||||
class: string = ""
|
||||
}
|
||||
functions {
|
||||
client function emitField(nameEvent, sourceEvent) { sourceEvent.stopPropagation(); output[nameEvent]({ value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent }) }
|
||||
client function preventReadonly(sourceEvent) { if (readonly) sourceEvent.preventDefault() }
|
||||
}
|
||||
view {
|
||||
<label class="wire-next wire-next--field wire-next--color-picker {class}"><span>{label}</span><input {...attrs} type="{type}" name="{name}" value="{value}" placeholder="{placeholder}" min="{min}" max="{max}" step="{step}" disabled="{disabled}" required="{required}" /></label>
|
||||
<div {...attrs} class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--color-picker {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}">
|
||||
<div class="wire-next__field-heading"><label class="{hiddenLabel ? 'wire-next__sr-only' : ''}" for="{id || name}">{label}</label>{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}</div>
|
||||
<div class="wire-next__color-control" data-icon-position="{iconPosition}">{#if icon}<span class="{icon}" aria-hidden="true"></span>{/if}<input id="{id || name}" type="color" name="{name}" value="{value}" disabled="{disabled}" required="{required}" readonly="{readonly}" aria-invalid="{error ? 'true' : 'false'}" @click="preventReadonly(event)" @input="emitField('input', event)" @change="emitField('change', event)" @focus="emitField('focus', event)" @blur="emitField('blur', event)" /><output>{value}</output></div>
|
||||
{#if helperText}<small class="wire-next__field-help">{helperText}</small>{/if}<small class="wire-next__field-error" data-error="{name}">{error}</small>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,35 @@
|
||||
component Columns {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
columns = 2
|
||||
gap = "md"
|
||||
maxWidth = "xl"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
columns: number = 2
|
||||
gap: string = "md"
|
||||
maxWidth: string = "xl"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--columns wire-next--gap-{gap} wire-next--columns-{columns} wire-next--max-{maxWidth} {class}"><slot /></div>
|
||||
<div
|
||||
data-ui-component="Columns"
|
||||
data-size='{size}'
|
||||
data-color='{color}'
|
||||
data-columns='{columns}'
|
||||
data-gap='{gap}'
|
||||
class='grid w-full grid-cols-1 items-start {class}'
|
||||
class:max-w-3xl='maxWidth === "md"'
|
||||
class:max-w-5xl='maxWidth === "lg"'
|
||||
class:max-w-7xl='maxWidth === "xl"'
|
||||
class:max-w-screen-2xl='maxWidth === "2xl"'
|
||||
class:max-w-none='maxWidth === "full"'
|
||||
class:md:grid-cols-2='columns >= 2'
|
||||
class:lg:grid-cols-3='columns === 3'
|
||||
class:lg:grid-cols-4='columns === 4'
|
||||
class:gap-3='gap === "sm"'
|
||||
class:gap-6='gap === "md"'
|
||||
class:gap-10='gap === "lg"'
|
||||
class:gap-14='gap === "xl"'
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,71 +1,74 @@
|
||||
component ComboBox {
|
||||
outputs {
|
||||
search(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
clear(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
open(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
close(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
load(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
error(payload: { error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "ComboBox"
|
||||
name = ""
|
||||
value = ""
|
||||
options = []
|
||||
groups = []
|
||||
placeholder = "Search or select an option"
|
||||
searchPlaceholder = "Start typing…"
|
||||
clearable = true
|
||||
allowCustomValue = false
|
||||
disabled = false
|
||||
required = false
|
||||
invalid = false
|
||||
validationMessage = ""
|
||||
helpText = ""
|
||||
loading = false
|
||||
loadingLabel = "Loading suggestions…"
|
||||
emptyLabel = "No matching options"
|
||||
clearLabel = "Clear value"
|
||||
toggleLabel = "Toggle suggestions"
|
||||
searchMode = "contains"
|
||||
searchFields = "label,description"
|
||||
minSearchLength = 0
|
||||
searchResultLimit = 0
|
||||
optionTemplate = "default"
|
||||
defaultOpen = false
|
||||
closeOnSelect = true
|
||||
fixed = false
|
||||
placement = "bottom"
|
||||
autocomplete = "off"
|
||||
remote = false
|
||||
remoteUrl = ""
|
||||
remoteQueryParam = "q"
|
||||
remoteDebounce = 250
|
||||
remoteAutoLoad = true
|
||||
infinite = false
|
||||
hasMore = false
|
||||
page = 1
|
||||
loadMoreLabel = "Load more"
|
||||
class = ""
|
||||
@event search = function
|
||||
@event select = function
|
||||
@event change = function
|
||||
@event clear = function
|
||||
@event open = function
|
||||
@event close = function
|
||||
@event load = function
|
||||
@event error = function
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
label: string = "ComboBox"
|
||||
name: string = ""
|
||||
value: string = ""
|
||||
options: unknown[] = []
|
||||
groups: unknown[] = []
|
||||
placeholder: string = "Search or select an option"
|
||||
searchPlaceholder: string = "Start typing…"
|
||||
clearable: boolean = true
|
||||
allowCustomValue: boolean = false
|
||||
disabled: boolean = false
|
||||
required: boolean = false
|
||||
invalid: boolean = false
|
||||
validationMessage: string = ""
|
||||
helpText: string = ""
|
||||
loading: boolean = false
|
||||
loadingLabel: string = "Loading suggestions…"
|
||||
emptyLabel: string = "No matching options"
|
||||
clearLabel: string = "Clear value"
|
||||
toggleLabel: string = "Toggle suggestions"
|
||||
searchMode: string = "contains"
|
||||
searchFields: string = "label,description"
|
||||
minSearchLength: number = 0
|
||||
searchResultLimit: number = 0
|
||||
optionTemplate: string = "default"
|
||||
defaultOpen: boolean = false
|
||||
closeOnSelect: boolean = true
|
||||
fixed: boolean = false
|
||||
placement: string = "bottom"
|
||||
autocomplete: string = "off"
|
||||
remote: boolean = false
|
||||
remoteUrl: string = ""
|
||||
remoteQueryParam: string = "q"
|
||||
remoteDebounce: number = 250
|
||||
remoteAutoLoad: boolean = true
|
||||
infinite: boolean = false
|
||||
hasMore: boolean = false
|
||||
page: number = 1
|
||||
loadMoreLabel: string = "Load more"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
state open = defaultOpen
|
||||
state query = ""
|
||||
state query: string = ""
|
||||
state selectedValue = value
|
||||
state activeIndex = -1
|
||||
state activeIndex: number = -1
|
||||
|
||||
functions {
|
||||
function allOptions() {
|
||||
shared function allOptions() {
|
||||
return [...groups.flatMap((group) => group.options || []), ...options]
|
||||
}
|
||||
|
||||
function searchableText(option) {
|
||||
shared function searchableText(option) {
|
||||
return ((option.label || "") + " " + (option.description || "")).toLowerCase()
|
||||
}
|
||||
|
||||
function matches(option) {
|
||||
shared function matches(option) {
|
||||
if (!query || query.length < minSearchLength) {
|
||||
return true
|
||||
}
|
||||
@@ -78,38 +81,38 @@ component ComboBox {
|
||||
return searchableText(option).includes(query.toLowerCase())
|
||||
}
|
||||
|
||||
function matchingOptions(list) {
|
||||
shared function matchingOptions(list) {
|
||||
return list.filter((option) => matches(option))
|
||||
}
|
||||
|
||||
function visibleOptions(list) {
|
||||
shared function visibleOptions(list) {
|
||||
if (searchResultLimit > 0) {
|
||||
return matchingOptions(list).slice(0, searchResultLimit)
|
||||
}
|
||||
return matchingOptions(list)
|
||||
}
|
||||
|
||||
function flatVisibleOptions() {
|
||||
shared function flatVisibleOptions() {
|
||||
return visibleOptions(allOptions())
|
||||
}
|
||||
|
||||
function isSelected(option) {
|
||||
shared function isSelected(option) {
|
||||
return selectedValue === option.value
|
||||
}
|
||||
|
||||
function selectedOptions() {
|
||||
shared function selectedOptions() {
|
||||
return allOptions().filter((option) => isSelected(option))
|
||||
}
|
||||
|
||||
function selectedText() {
|
||||
shared function selectedText() {
|
||||
return selectedOptions().length ? selectedOptions()[0].label : selectedValue
|
||||
}
|
||||
|
||||
function inputText() {
|
||||
shared function inputText() {
|
||||
return query || selectedText()
|
||||
}
|
||||
|
||||
function shouldShowDropdown() {
|
||||
shared function shouldShowDropdown() {
|
||||
if (!open) {
|
||||
return false
|
||||
}
|
||||
@@ -125,11 +128,11 @@ component ComboBox {
|
||||
return infinite && hasMore
|
||||
}
|
||||
|
||||
function canSelect(option) {
|
||||
shared function canSelect(option) {
|
||||
return !option.disabled
|
||||
}
|
||||
|
||||
function chooseOption(option) {
|
||||
shared function chooseOption(option) {
|
||||
if (!canSelect(option)) {
|
||||
return
|
||||
}
|
||||
@@ -141,14 +144,14 @@ component ComboBox {
|
||||
}
|
||||
}
|
||||
|
||||
function updateQuery(event) {
|
||||
client function updateQuery(event) {
|
||||
query = event.target.value
|
||||
selectedValue = allowCustomValue ? event.target.value : ""
|
||||
open = true
|
||||
activeIndex = flatVisibleOptions().length ? 0 : -1
|
||||
}
|
||||
|
||||
function clearValue(event) {
|
||||
client function clearValue(event) {
|
||||
if (event) {
|
||||
event.stopPropagation()
|
||||
}
|
||||
@@ -158,18 +161,18 @@ component ComboBox {
|
||||
open = false
|
||||
}
|
||||
|
||||
function openDropdown() {
|
||||
shared function openDropdown() {
|
||||
if (!disabled) {
|
||||
open = true
|
||||
activeIndex = flatVisibleOptions().length ? 0 : -1
|
||||
}
|
||||
}
|
||||
|
||||
function closeDropdown() {
|
||||
shared function closeDropdown() {
|
||||
open = false
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
shared function toggle() {
|
||||
if (open) {
|
||||
closeDropdown()
|
||||
} else {
|
||||
@@ -177,19 +180,19 @@ component ComboBox {
|
||||
}
|
||||
}
|
||||
|
||||
function setValue(nextValue) {
|
||||
shared function setValue(nextValue) {
|
||||
selectedValue = nextValue
|
||||
query = ""
|
||||
}
|
||||
|
||||
function moveActive(direction) {
|
||||
shared function moveActive(direction) {
|
||||
if (!flatVisibleOptions().length) {
|
||||
return
|
||||
}
|
||||
activeIndex = (activeIndex + direction + flatVisibleOptions().length) % flatVisibleOptions().length
|
||||
}
|
||||
|
||||
function handleKeydown(event) {
|
||||
client function handleKeydown(event) {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
@@ -215,7 +218,7 @@ component ComboBox {
|
||||
}
|
||||
}
|
||||
|
||||
function optionIndex(option) {
|
||||
shared function optionIndex(option) {
|
||||
return flatVisibleOptions().findIndex((item) => item.value === option.value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
component Confetti {
|
||||
outputs {
|
||||
start(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
complete(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Confetti"
|
||||
description = ""
|
||||
items = []
|
||||
variant = "default"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
title: string = "Confetti"
|
||||
description: string = ""
|
||||
items: unknown[] = []
|
||||
variant: string = "default"
|
||||
class: string = ""
|
||||
}
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--confetti wire-next--variant-{variant} {class}">
|
||||
|
||||
@@ -1,19 +1,521 @@
|
||||
component ContextMenu {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Context Menu"
|
||||
description = ""
|
||||
open = false
|
||||
placement = "bottom"
|
||||
closeLabel = "Close"
|
||||
class = ""
|
||||
outputs {
|
||||
open(payload: { x: number; y: number; trigger: string; sourceEvent: Event })
|
||||
close(payload: { reason: string; sourceEvent: Event })
|
||||
select(payload: { item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })
|
||||
action(payload: { item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })
|
||||
}
|
||||
|
||||
props {
|
||||
|
||||
items: unknown[] = []
|
||||
open: boolean = false
|
||||
defaultOpen: boolean = false
|
||||
trigger: string = "contextmenu"
|
||||
placement: string = "pointer"
|
||||
align: string = "start"
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
variant: string = "raised"
|
||||
title: string = ""
|
||||
description: string = ""
|
||||
label: string = "Context menu"
|
||||
closeOnSelect: boolean = true
|
||||
closeOnOutside: boolean = true
|
||||
disabled: boolean = false
|
||||
minWidth: string = "14rem"
|
||||
maxWidth: string = "20rem"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
state visible = defaultOpen
|
||||
state positionX: number = 16
|
||||
state positionY: number = 16
|
||||
|
||||
functions {
|
||||
shared function isOpen() {
|
||||
return open || visible
|
||||
}
|
||||
|
||||
client function showMenu(sourceEvent) {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
if (sourceEvent && sourceEvent.type === "contextmenu") {
|
||||
sourceEvent.preventDefault()
|
||||
}
|
||||
if (placement === "pointer" && sourceEvent) {
|
||||
positionX = Math.max(12, Math.min(sourceEvent.clientX || 12, window.innerWidth - 340))
|
||||
positionY = Math.max(12, Math.min(sourceEvent.clientY || 12, window.innerHeight - 420))
|
||||
}
|
||||
visible = true
|
||||
output.open({
|
||||
x: positionX,
|
||||
y: positionY,
|
||||
trigger: trigger,
|
||||
sourceEvent: sourceEvent
|
||||
})
|
||||
}
|
||||
|
||||
client function hideMenu(reason, sourceEvent) {
|
||||
visible = false
|
||||
output.close({
|
||||
reason: reason,
|
||||
sourceEvent: sourceEvent
|
||||
})
|
||||
}
|
||||
|
||||
client function toggleMenu(sourceEvent) {
|
||||
if (isOpen()) {
|
||||
hideMenu("toggle", sourceEvent)
|
||||
} else {
|
||||
showMenu(sourceEvent)
|
||||
}
|
||||
}
|
||||
|
||||
client function chooseItem(item, itemIndex, sourceEvent) {
|
||||
if (disabled || item.disabled || item.type === "header" || item.type === "divider") {
|
||||
sourceEvent.preventDefault()
|
||||
return
|
||||
}
|
||||
output.select({
|
||||
item: item,
|
||||
itemIndex: itemIndex,
|
||||
value: item.value || "",
|
||||
sourceEvent: sourceEvent
|
||||
})
|
||||
if (item.action) {
|
||||
output.action({
|
||||
item: item,
|
||||
itemIndex: itemIndex,
|
||||
value: item.value || "",
|
||||
sourceEvent: sourceEvent
|
||||
})
|
||||
}
|
||||
if (closeOnSelect) {
|
||||
hideMenu("select", sourceEvent)
|
||||
}
|
||||
}
|
||||
|
||||
client function moveFocus(sourceEvent, direction) {
|
||||
const root = sourceEvent.currentTarget.closest(".wire-context-menu") || sourceEvent.currentTarget
|
||||
const options = [...root.querySelectorAll("[data-context-menu-item]:not([disabled])")]
|
||||
if (!options.length) {
|
||||
return
|
||||
}
|
||||
const activeIndex = options.indexOf(document.activeElement)
|
||||
const nextIndex = (activeIndex + direction + options.length) % options.length
|
||||
options[nextIndex].focus()
|
||||
}
|
||||
|
||||
client function handleKeydown(sourceEvent) {
|
||||
if (sourceEvent.key === "Escape") {
|
||||
sourceEvent.preventDefault()
|
||||
hideMenu("escape", sourceEvent)
|
||||
} else if (sourceEvent.key === "ArrowDown") {
|
||||
sourceEvent.preventDefault()
|
||||
moveFocus(sourceEvent, 1)
|
||||
} else if (sourceEvent.key === "ArrowUp") {
|
||||
sourceEvent.preventDefault()
|
||||
moveFocus(sourceEvent, -1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<div data-show="{open}" class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--context-menu wire-next--placement-{placement} {class}" role="region" aria-modal="false" aria-label="{title}">
|
||||
<header><strong>{title}</strong><button type="button" aria-label="{closeLabel}">×</button></header>
|
||||
{#if description}<p>{description}</p>{/if}
|
||||
<slot />
|
||||
<div
|
||||
{...attrs}
|
||||
data-ui-component="ContextMenu"
|
||||
data-open='{open || visible ? "true" : "false"}'
|
||||
data-trigger='{trigger}'
|
||||
data-placement='{placement}'
|
||||
data-align='{align}'
|
||||
data-size='{size}'
|
||||
data-color='{color}'
|
||||
data-variant='{variant}'
|
||||
class='wire-context-menu {class}'
|
||||
style='--wire-context-x:{positionX}px;--wire-context-y:{positionY}px;--wire-context-min-width:{minWidth};--wire-context-max-width:{maxWidth};'
|
||||
@keydown='handleKeydown(event)'
|
||||
>
|
||||
<div
|
||||
class="wire-context-menu__trigger"
|
||||
tabindex='{disabled ? "-1" : "0"}'
|
||||
aria-haspopup="menu"
|
||||
aria-expanded='{open || visible ? "true" : "false"}'
|
||||
@contextmenu='if (trigger === "contextmenu" || trigger === "both") { showMenu(event) }'
|
||||
@click='if (trigger === "click" || trigger === "both") { toggleMenu(event) }'
|
||||
@keydown='if (event.key === "Enter" || event.key === " ") { event.preventDefault(); toggleMenu(event) }'
|
||||
>
|
||||
<slot name="trigger"></slot>
|
||||
</div>
|
||||
|
||||
{#if closeOnOutside}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-context-menu__dismiss-layer"
|
||||
data-show='{open || visible}'
|
||||
aria-label="Close context menu"
|
||||
@click='hideMenu("outside", event)'
|
||||
></button>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="wire-context-menu__panel"
|
||||
data-show='{open || visible}'
|
||||
role="menu"
|
||||
aria-label='{label}'
|
||||
>
|
||||
{#if title || description}
|
||||
<div class="wire-context-menu__header">
|
||||
{#if title}
|
||||
<strong>{title}</strong>
|
||||
{/if}
|
||||
{#if description}
|
||||
<span>{description}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<slot name="header"></slot>
|
||||
|
||||
<div class="wire-context-menu__items">
|
||||
{#each items as item, itemIndex}
|
||||
{#if item.type === "divider"}
|
||||
<div class="wire-context-menu__divider" role="separator"></div>
|
||||
{:else if item.type === "header"}
|
||||
<div class="wire-context-menu__section-label">{item.label || item.title}</div>
|
||||
{:else if item.href}
|
||||
<a
|
||||
href='{item.href}'
|
||||
target='{item.target || ""}'
|
||||
rel='{item.external || item.target === "_blank" ? "noopener noreferrer" : (item.rel || "")}'
|
||||
role="menuitem"
|
||||
data-context-menu-item
|
||||
data-danger='{item.danger ? "true" : "false"}'
|
||||
data-selected='{item.selected || item.checked ? "true" : "false"}'
|
||||
aria-disabled='{item.disabled ? "true" : "false"}'
|
||||
class="wire-context-menu__item"
|
||||
@click='chooseItem(item, itemIndex, event)'
|
||||
>
|
||||
{#if item.icon}
|
||||
<span class='wire-context-menu__icon {item.icon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span class="wire-context-menu__copy">
|
||||
<strong>{item.label || item.title}</strong>
|
||||
{#if item.description}
|
||||
<small>{item.description}</small>
|
||||
{/if}
|
||||
</span>
|
||||
{#if item.shortcut}
|
||||
<kbd>{item.shortcut}</kbd>
|
||||
{:else if item.checked || item.selected}
|
||||
<span class="icon-[lucide--check] wire-context-menu__status" aria-hidden="true"></span>
|
||||
{:else if item.external}
|
||||
<span class="icon-[lucide--arrow-up-right] wire-context-menu__status" aria-hidden="true"></span>
|
||||
{/if}
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
data-context-menu-item
|
||||
data-danger='{item.danger ? "true" : "false"}'
|
||||
data-selected='{item.selected || item.checked ? "true" : "false"}'
|
||||
disabled='{item.disabled}'
|
||||
class="wire-context-menu__item"
|
||||
@click='chooseItem(item, itemIndex, event)'
|
||||
>
|
||||
{#if item.icon}
|
||||
<span class='wire-context-menu__icon {item.icon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span class="wire-context-menu__copy">
|
||||
<strong>{item.label || item.title}</strong>
|
||||
{#if item.description}
|
||||
<small>{item.description}</small>
|
||||
{/if}
|
||||
</span>
|
||||
{#if item.shortcut}
|
||||
<kbd>{item.shortcut}</kbd>
|
||||
{:else if item.checked || item.selected}
|
||||
<span class="icon-[lucide--check] wire-context-menu__status" aria-hidden="true"></span>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<slot></slot>
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-context-menu {
|
||||
--context-accent: var(--wire-color-primary);
|
||||
--context-soft: var(--wire-color-primary-soft);
|
||||
position: relative;
|
||||
display: block;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-context-menu[data-color="secondary"] {
|
||||
--context-accent: var(--wire-color-secondary);
|
||||
--context-soft: var(--wire-color-secondary-soft);
|
||||
}
|
||||
|
||||
.wire-context-menu[data-color="info"] {
|
||||
--context-accent: var(--wire-color-info);
|
||||
--context-soft: var(--wire-color-info-soft);
|
||||
}
|
||||
|
||||
.wire-context-menu[data-color="success"] {
|
||||
--context-accent: var(--wire-color-success);
|
||||
--context-soft: var(--wire-color-success-soft);
|
||||
}
|
||||
|
||||
.wire-context-menu[data-color="warning"] {
|
||||
--context-accent: var(--wire-color-warning);
|
||||
--context-soft: var(--wire-color-warning-soft);
|
||||
}
|
||||
|
||||
.wire-context-menu[data-color="danger"] {
|
||||
--context-accent: var(--wire-color-danger);
|
||||
--context-soft: var(--wire-color-danger-soft);
|
||||
}
|
||||
|
||||
.wire-context-menu__trigger {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.wire-context-menu__trigger:focus-visible {
|
||||
border-radius: 0.6rem;
|
||||
outline: 2px solid var(--wire-color-focus);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.wire-context-menu__dismiss-layer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1090;
|
||||
appearance: none;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.wire-context-menu__panel {
|
||||
position: absolute;
|
||||
z-index: 1091;
|
||||
top: calc(100% + 0.5rem);
|
||||
left: 0;
|
||||
width: max-content;
|
||||
min-width: var(--wire-context-min-width);
|
||||
max-width: min(var(--wire-context-max-width), calc(100vw - 1.5rem));
|
||||
padding: 0.45rem;
|
||||
color: var(--wire-color-text);
|
||||
background:
|
||||
linear-gradient(145deg, color-mix(in srgb, var(--context-accent) 5%, transparent), transparent 58%),
|
||||
color-mix(in srgb, var(--wire-color-surface-raised) 96%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--context-accent) 18%, var(--wire-color-border));
|
||||
border-radius: 1rem;
|
||||
box-shadow:
|
||||
0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
|
||||
0 24px 64px color-mix(in srgb, black 24%, transparent);
|
||||
backdrop-filter: blur(18px);
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
.wire-context-menu[data-placement="pointer"] .wire-context-menu__panel {
|
||||
position: fixed;
|
||||
top: var(--wire-context-y);
|
||||
left: var(--wire-context-x);
|
||||
}
|
||||
|
||||
.wire-context-menu[data-placement="bottom-end"] .wire-context-menu__panel {
|
||||
right: 0;
|
||||
left: auto;
|
||||
transform-origin: top right;
|
||||
}
|
||||
|
||||
.wire-context-menu[data-placement="top-start"] .wire-context-menu__panel {
|
||||
top: auto;
|
||||
bottom: calc(100% + 0.5rem);
|
||||
transform-origin: bottom left;
|
||||
}
|
||||
|
||||
.wire-context-menu[data-placement="top-end"] .wire-context-menu__panel {
|
||||
top: auto;
|
||||
right: 0;
|
||||
bottom: calc(100% + 0.5rem);
|
||||
left: auto;
|
||||
transform-origin: bottom right;
|
||||
}
|
||||
|
||||
.wire-context-menu[data-variant="soft"] .wire-context-menu__panel {
|
||||
background:
|
||||
linear-gradient(145deg, var(--context-soft), transparent 72%),
|
||||
var(--wire-color-surface-raised);
|
||||
box-shadow: 0 18px 48px color-mix(in srgb, black 16%, transparent);
|
||||
}
|
||||
|
||||
.wire-context-menu[data-variant="outline"] .wire-context-menu__panel {
|
||||
background: var(--wire-color-surface-raised);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wire-context-menu__header {
|
||||
display: grid;
|
||||
gap: 0.2rem;
|
||||
padding: 0.65rem 0.75rem 0.75rem;
|
||||
border-bottom: 1px solid var(--wire-color-border);
|
||||
}
|
||||
|
||||
.wire-context-menu__header strong {
|
||||
font-size: 0.86rem;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.wire-context-menu__header span {
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.wire-context-menu__items {
|
||||
display: grid;
|
||||
gap: 0.15rem;
|
||||
padding-block: 0.25rem;
|
||||
}
|
||||
|
||||
.wire-context-menu__section-label {
|
||||
padding: 0.55rem 0.75rem 0.3rem;
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.wire-context-menu__divider {
|
||||
height: 1px;
|
||||
margin: 0.3rem 0.45rem;
|
||||
background: var(--wire-color-border);
|
||||
}
|
||||
|
||||
.wire-context-menu__item {
|
||||
appearance: none;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 0.7rem;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 0.65rem 0.7rem;
|
||||
color: var(--wire-color-text);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 0.72rem;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 150ms ease, color 150ms ease, transform 150ms ease;
|
||||
}
|
||||
|
||||
.wire-context-menu__item:hover,
|
||||
.wire-context-menu__item:focus-visible,
|
||||
.wire-context-menu__item[data-selected="true"] {
|
||||
color: var(--context-accent);
|
||||
background: var(--context-soft);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.wire-context-menu__item:active {
|
||||
transform: scale(0.985);
|
||||
}
|
||||
|
||||
.wire-context-menu__item[data-danger="true"] {
|
||||
color: var(--wire-color-danger);
|
||||
}
|
||||
|
||||
.wire-context-menu__item[data-danger="true"]:hover,
|
||||
.wire-context-menu__item[data-danger="true"]:focus-visible {
|
||||
background: var(--wire-color-danger-soft);
|
||||
}
|
||||
|
||||
.wire-context-menu__item[disabled],
|
||||
.wire-context-menu__item[aria-disabled="true"] {
|
||||
opacity: 0.48;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wire-context-menu__icon,
|
||||
.wire-context-menu__status {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.wire-context-menu__copy {
|
||||
display: grid;
|
||||
gap: 0.12rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-context-menu__copy strong {
|
||||
overflow: hidden;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wire-context-menu__copy small {
|
||||
overflow: hidden;
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.35;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wire-context-menu kbd {
|
||||
padding: 0.16rem 0.38rem;
|
||||
color: var(--wire-color-text-muted);
|
||||
background: var(--wire-color-surface-soft);
|
||||
border: 1px solid var(--wire-color-border);
|
||||
border-radius: 0.36rem;
|
||||
font-size: 0.64rem;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.wire-context-menu[data-size="sm"] .wire-context-menu__item {
|
||||
padding: 0.52rem 0.6rem;
|
||||
}
|
||||
|
||||
.wire-context-menu[data-size="lg"] .wire-context-menu__item {
|
||||
padding: 0.78rem 0.82rem;
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.wire-context-menu[data-placement="pointer"] .wire-context-menu__panel {
|
||||
right: 0.75rem;
|
||||
bottom: 0.75rem;
|
||||
left: 0.75rem;
|
||||
top: auto;
|
||||
width: auto;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wire-context-menu__item {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
component CopyMarkup {
|
||||
outputs {
|
||||
copy(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
|
||||
success(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
error(payload: { error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "Copy Markup"
|
||||
name = ""
|
||||
value = ""
|
||||
placeholder = ""
|
||||
type = "text"
|
||||
min = ""
|
||||
max = ""
|
||||
step = ""
|
||||
disabled = false
|
||||
required = false
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
label: string = "Copy Markup"
|
||||
name: string = ""
|
||||
value: string = ""
|
||||
placeholder: string = ""
|
||||
type: string = "text"
|
||||
min: string = ""
|
||||
max: string = ""
|
||||
step: string = ""
|
||||
disabled: boolean = false
|
||||
required: boolean = false
|
||||
class: string = ""
|
||||
}
|
||||
view {
|
||||
<label class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--copy-markup {class}"><span>{label}</span><input {...attrs} type="{type}" name="{name}" value="{value}" placeholder="{placeholder}" min="{min}" max="{max}" step="{step}" disabled="{disabled}" required="{required}" /></label>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
component CustomScrollbar {
|
||||
outputs {
|
||||
scroll(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
columns = 2
|
||||
gap = "md"
|
||||
maxWidth = "xl"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
columns: number = 2
|
||||
gap: string = "md"
|
||||
maxWidth: string = "xl"
|
||||
class: string = ""
|
||||
}
|
||||
view {
|
||||
<div class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--custom-scrollbar wire-next--gap-{gap} wire-next--columns-{columns} wire-next--max-{maxWidth} {class}"><slot /></div>
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
component DataMap {
|
||||
outputs {
|
||||
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Data Map"
|
||||
description = ""
|
||||
items = []
|
||||
variant = "default"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
title: string = "Data Map"
|
||||
description: string = ""
|
||||
items: unknown[] = []
|
||||
variant: string = "default"
|
||||
class: string = ""
|
||||
}
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--data-map wire-next--variant-{variant} {class}">
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
component DataTable {
|
||||
outputs {
|
||||
sort(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
rowClick(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
pageChange(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
caption = "Data Table"
|
||||
columns = []
|
||||
rows = []
|
||||
striped = true
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
caption: string = "Data Table"
|
||||
columns: unknown[] = []
|
||||
rows: unknown[] = []
|
||||
striped: boolean = true
|
||||
class: string = ""
|
||||
}
|
||||
view {
|
||||
<div class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--table {class}"><table><caption>{caption}</caption><thead><tr>{#each columns as column}<th>{column.label}</th>{/each}</tr></thead><tbody>{#each rows as row}<tr>{#each columns as column}<td>{row[column.key]}</td>{/each}</tr>{/each}</tbody></table><slot /></div>
|
||||
|
||||
@@ -1,20 +1,62 @@
|
||||
component DatePicker {
|
||||
outputs {
|
||||
input(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
|
||||
change(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
|
||||
open(payload: { value: string; name: string; sourceEvent: Event })
|
||||
close(payload: { value: string; name: string; sourceEvent: Event })
|
||||
focus(payload: { value: string; name: string; sourceEvent: Event })
|
||||
blur(payload: { value: string; name: string; sourceEvent: Event })
|
||||
invalid(payload: { name: string; message: string; sourceEvent: Event })
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "Date Picker"
|
||||
name = ""
|
||||
value = ""
|
||||
placeholder = ""
|
||||
type = "date"
|
||||
min = ""
|
||||
max = ""
|
||||
step = ""
|
||||
disabled = false
|
||||
required = false
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
label: string = "Date Picker"
|
||||
id: string = ""
|
||||
name: string = ""
|
||||
value: string = ""
|
||||
placeholder: string = ""
|
||||
type: string = "date"
|
||||
locale: string = "en-US"
|
||||
firstDayOfWeek: number = 0
|
||||
months = [{ label: "Jan", value: "01" }, { label: "Feb", value: "02" }, { label: "Mar", value: "03" }, { label: "Apr", value: "04" }, { label: "May", value: "05" }, { label: "Jun", value: "06" }, { label: "Jul", value: "07" }, { label: "Aug", value: "08" }, { label: "Sep", value: "09" }, { label: "Oct", value: "10" }, { label: "Nov", value: "11" }, { label: "Dec", value: "12" }]
|
||||
days = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]
|
||||
years = [2024, 2025, 2026, 2027, 2028, 2029, 2030]
|
||||
min: string = ""
|
||||
max: string = ""
|
||||
step: string = ""
|
||||
helperText: string = ""
|
||||
cornerHint: string = ""
|
||||
error: string = ""
|
||||
variant: string = "normal"
|
||||
inline: boolean = false
|
||||
readonly: boolean = false
|
||||
disabled: boolean = false
|
||||
required: boolean = false
|
||||
class: string = ""
|
||||
}
|
||||
state currentValue = value
|
||||
state selectedYear = value ? value.split("-")[0] : ""
|
||||
state selectedMonth = value ? value.split("-")[1] : ""
|
||||
state selectedDay = value ? value.split("-")[2] : ""
|
||||
state expanded: boolean = false
|
||||
functions {
|
||||
client function openCalendar(sourceEvent) { expanded = true; output.open({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
|
||||
client function updateDate(part, nextValue, sourceEvent, nextDate, detail) { if (part === "year") { selectedYear = String(nextValue) } if (part === "month") { selectedMonth = String(nextValue).padStart(2, "0") } if (part === "day") { selectedDay = String(nextValue).padStart(2, "0") } if (!selectedYear || !selectedMonth || !selectedDay) { return } nextDate = selectedYear + "-" + selectedMonth + "-" + selectedDay; if ((min && nextDate < min) || (max && nextDate > max)) { return } currentValue = nextDate; detail = { value: currentValue, year: selectedYear, month: selectedMonth, day: selectedDay, name: name, sourceEvent: sourceEvent }; output.input(detail); output.change(detail) }
|
||||
client function finishDate(sourceEvent) { if (!currentValue) { return } expanded = false; output.close({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
|
||||
client function selectDay(sourceEvent, root, input, nextDate, trigger, detail) { root = sourceEvent.currentTarget.closest(".wire-next--date-picker"); input = root.querySelector(".wire-next__picker-value"); nextDate = input.getAttribute("value").slice(0, 5) + root.querySelector("select").getAttribute("value") + "-" + sourceEvent.currentTarget.dataset.value; currentValue = nextDate; input.setAttribute("value", nextDate); trigger = root.querySelector(".wire-next__date-trigger span"); if (trigger) { trigger.replaceChildren(nextDate) } sourceEvent.currentTarget.setAttribute("aria-pressed", "true"); detail = { value: nextDate, name: name, sourceEvent: sourceEvent }; output.input(detail); output.change(detail) }
|
||||
client function clearDate(sourceEvent, detail) { if (disabled || readonly) { return } currentValue = ""; detail = { value: currentValue, name: name, sourceEvent: sourceEvent }; output.input(detail); output.change(detail) }
|
||||
client function handleFocus(sourceEvent) { output.focus({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
|
||||
client function handleBlur(sourceEvent) { output.blur({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
|
||||
client function handleInvalid(sourceEvent) { output.invalid({ name: name, message: sourceEvent.currentTarget.validationMessage, sourceEvent: sourceEvent }) }
|
||||
}
|
||||
view {
|
||||
<label class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--date-picker {class}"><span>{label}</span><input {...attrs} type="{type}" name="{name}" value="{value}" placeholder="{placeholder}" min="{min}" max="{max}" step="{step}" disabled="{disabled}" required="{required}" /></label>
|
||||
<div {...attrs} class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--date-picker {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}" data-expanded="{expanded}">
|
||||
<div class="wire-next__field-heading"><label for="{id || name}">{label}</label>{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}</div>
|
||||
<div class="wire-next__picker-control"><input id="{id || name}" class="wire-next__sr-only wire-next__picker-value" type="text" name="{name}" value="{currentValue}" required="{required}" readonly aria-hidden="true" tabindex="-1" @invalid="handleInvalid(event)" /><button type="button" class="wire-next__date-trigger" disabled="{disabled || readonly}" aria-haspopup="dialog" aria-expanded="{expanded}" @click="openCalendar(event)" @focus="handleFocus(event)" @blur="handleBlur(event)"><span>{currentValue || placeholder || "Select date"}</span><i class="icon-[lucide--calendar-days]" aria-hidden="true"></i></button>{#if currentValue && !readonly && !disabled}<button type="button" class="wire-next__picker-clear" aria-label="Clear date" @click="clearDate(event)">×</button>{/if}</div>
|
||||
<div class="wire-next__calendar" role="dialog" aria-label="{label}"><div class="wire-next__date-selectors"><label>Month<select value="{selectedMonth}" @change="updateDate('month', event.currentTarget.value, event)">{#each months as month}<option value="{month.value}" selected="{month.value === selectedMonth}">{month.label}</option>{/each}</select></label><label>Year<select value="{selectedYear}" @change="updateDate('year', event.currentTarget.value, event)">{#each years as year}<option value="{year}" selected="{String(year) === selectedYear}">{year}</option>{/each}</select></label></div><div class="wire-next__calendar-grid">{#each days as day}<button type="button" data-value="{String(day).padStart(2, '0')}" aria-pressed="{String(day).padStart(2, '0') === selectedDay}" @click="selectDay(event)">{day}</button>{/each}</div><button type="button" class="wire-next__date-done" disabled="{!currentValue}" @click="finishDate(event)">Done</button></div>
|
||||
{#if helperText}<small class="wire-next__field-help">{helperText}</small>{/if}<small class="wire-next__field-error" data-error="{name}">{error}</small>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,34 @@
|
||||
component DeviceFrame {
|
||||
outputs {
|
||||
change(payload: { device: string; orientation: string; sourceEvent: Event })
|
||||
rotate(payload: { device: string; orientation: string; sourceEvent: Event })
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Device Frame"
|
||||
description = ""
|
||||
items = []
|
||||
variant = "default"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
title: string = "Device Frame"
|
||||
description: string = ""
|
||||
items: unknown[] = []
|
||||
variant: string = "default"
|
||||
device: string = "phone"
|
||||
orientation: string = "portrait"
|
||||
src: string = ""
|
||||
srcdoc: string = ""
|
||||
frameTitle: string = "Device preview"
|
||||
showToolbar: boolean = true
|
||||
allow: string = ""
|
||||
class: string = ""
|
||||
}
|
||||
state currentOrientation = orientation
|
||||
functions {
|
||||
client function setDevice(nextDevice, sourceEvent) { output.change({ device: nextDevice, orientation: currentOrientation, sourceEvent: sourceEvent }) }
|
||||
client function rotateFrame(sourceEvent) { currentOrientation = currentOrientation === "portrait" ? "landscape" : "portrait"; output.rotate({ device: device, orientation: currentOrientation, sourceEvent: sourceEvent }); output.change({ device: device, orientation: currentOrientation, sourceEvent: sourceEvent }) }
|
||||
}
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--device-frame wire-next--variant-{variant} {class}">
|
||||
{#if title}<strong>{title}</strong>{/if}
|
||||
{#if description}<p>{description}</p>{/if}
|
||||
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
|
||||
<slot />
|
||||
<section {...attrs} class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--device-frame wire-next--variant-{variant} {class}" data-device="{device}" data-orientation="{currentOrientation}">
|
||||
<header><div>{#if title}<strong>{title}</strong>{/if}{#if description}<p>{description}</p>{/if}</div>{#if showToolbar}<div class="wire-next__device-actions" role="toolbar" aria-label="Preview device">{#each items as item}<button type="button" aria-pressed="{item.value === device}" @click="setDevice(item.value, event)">{item.label}</button>{/each}<button type="button" aria-label="Rotate preview" @click="rotateFrame(event)">↻</button></div>{/if}</header>
|
||||
<div class="wire-next__device-shell">{#if src || srcdoc}<iframe title="{frameTitle}" src="{src}" srcdoc="{srcdoc}" allow="{allow}"></iframe>{:else}<div class="wire-next__device-content"><slot /></div>{/if}</div>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
component DragAndDrop {
|
||||
outputs {
|
||||
dragStart(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
dragEnd(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
dragEnter(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
dragLeave(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
drop(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Drag And Drop"
|
||||
description = ""
|
||||
items = []
|
||||
variant = "default"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
title: string = "Drag And Drop"
|
||||
description: string = ""
|
||||
items: unknown[] = []
|
||||
variant: string = "default"
|
||||
class: string = ""
|
||||
}
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--drag-and-drop wire-next--variant-{variant} {class}">
|
||||
|
||||
@@ -1,19 +1,453 @@
|
||||
component Drawer {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Drawer"
|
||||
description = ""
|
||||
open = false
|
||||
placement = "bottom"
|
||||
closeLabel = "Close"
|
||||
class = ""
|
||||
outputs {
|
||||
open(payload: { placement: string; sourceEvent: Event })
|
||||
close(payload: { reason: string; placement: string; sourceEvent: Event })
|
||||
cancel(payload: { placement: string; sourceEvent: Event })
|
||||
}
|
||||
|
||||
props {
|
||||
open: boolean = false
|
||||
defaultOpen: boolean = false
|
||||
placement: string = "right"
|
||||
size: string = "md"
|
||||
color: string = "primary"
|
||||
variant: string = "default"
|
||||
title: string = "Drawer"
|
||||
description: string = ""
|
||||
icon: string = ""
|
||||
label: string = "Drawer"
|
||||
closeLabel: string = "Close drawer"
|
||||
showClose: boolean = true
|
||||
closeOnBackdrop: boolean = true
|
||||
closeOnEscape: boolean = true
|
||||
overlay: boolean = true
|
||||
scrollable: boolean = true
|
||||
triggerLabel: string = ""
|
||||
triggerIcon: string = ""
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
state visible = defaultOpen
|
||||
|
||||
functions {
|
||||
shared function isOpen() {
|
||||
return open || visible
|
||||
}
|
||||
|
||||
client function showDrawer(sourceEvent) {
|
||||
visible = true
|
||||
output.open({
|
||||
placement: placement,
|
||||
sourceEvent: sourceEvent
|
||||
})
|
||||
}
|
||||
|
||||
client function hideDrawer(reason, sourceEvent) {
|
||||
visible = false
|
||||
output.close({
|
||||
reason: reason,
|
||||
placement: placement,
|
||||
sourceEvent: sourceEvent
|
||||
})
|
||||
}
|
||||
|
||||
client function cancelDrawer(sourceEvent) {
|
||||
output.cancel({
|
||||
placement: placement,
|
||||
sourceEvent: sourceEvent
|
||||
})
|
||||
hideDrawer("cancel", sourceEvent)
|
||||
}
|
||||
|
||||
client function handleKeydown(sourceEvent) {
|
||||
if (closeOnEscape && sourceEvent.key === "Escape") {
|
||||
sourceEvent.preventDefault()
|
||||
cancelDrawer(sourceEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<div data-show="{open}" class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--drawer wire-next--placement-{placement} {class}" role="dialog" aria-modal="true" aria-label="{title}">
|
||||
<header><strong>{title}</strong><button type="button" aria-label="{closeLabel}">×</button></header>
|
||||
{#if description}<p>{description}</p>{/if}
|
||||
<slot />
|
||||
<div
|
||||
{...attrs}
|
||||
data-ui-component="Drawer"
|
||||
data-open='{open || visible ? "true" : "false"}'
|
||||
data-placement='{placement}'
|
||||
data-size='{size}'
|
||||
data-color='{color}'
|
||||
data-variant='{variant}'
|
||||
data-overlay='{overlay ? "true" : "false"}'
|
||||
data-scrollable='{scrollable ? "true" : "false"}'
|
||||
class='wire-drawer {class}'
|
||||
>
|
||||
{#if triggerLabel}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-drawer__trigger"
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded='{open || visible ? "true" : "false"}'
|
||||
@click='showDrawer(event)'
|
||||
>
|
||||
{#if triggerIcon}
|
||||
<span class='{triggerIcon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span>{triggerLabel}</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<span class="wire-drawer__trigger-slot" @click='showDrawer(event)'>
|
||||
<slot name="trigger"></slot>
|
||||
</span>
|
||||
|
||||
<div
|
||||
class="wire-drawer__layer"
|
||||
data-show='{open || visible}'
|
||||
role="presentation"
|
||||
@keydown='handleKeydown(event)'
|
||||
>
|
||||
{#if overlay}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-drawer__backdrop"
|
||||
aria-label='{closeLabel}'
|
||||
@click='if (closeOnBackdrop) { cancelDrawer(event) }'
|
||||
></button>
|
||||
{/if}
|
||||
|
||||
<section
|
||||
class="wire-drawer__panel"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label='{label || title}'
|
||||
tabindex="-1"
|
||||
>
|
||||
<div class="wire-drawer__handle" aria-hidden="true"></div>
|
||||
|
||||
<header class="wire-drawer__header">
|
||||
<div class="wire-drawer__heading">
|
||||
{#if icon}
|
||||
<span class='wire-drawer__icon {icon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
|
||||
<div class="wire-drawer__heading-copy">
|
||||
<slot name="header"></slot>
|
||||
{#if title}
|
||||
<h2>{title}</h2>
|
||||
{/if}
|
||||
{#if description}
|
||||
<p>{description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showClose}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-drawer__close"
|
||||
aria-label='{closeLabel}'
|
||||
@click='hideDrawer("close-button", event)'
|
||||
>
|
||||
<span class="icon-[lucide--x]" aria-hidden="true"></span>
|
||||
</button>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
<div class="wire-drawer__body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<footer class="wire-drawer__footer">
|
||||
<slot name="footer"></slot>
|
||||
</footer>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-drawer {
|
||||
--drawer-accent: var(--wire-color-primary);
|
||||
--drawer-soft: var(--wire-color-primary-soft);
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.wire-drawer[data-color="secondary"] {
|
||||
--drawer-accent: var(--wire-color-secondary);
|
||||
--drawer-soft: var(--wire-color-secondary-soft);
|
||||
}
|
||||
|
||||
.wire-drawer[data-color="info"] {
|
||||
--drawer-accent: var(--wire-color-info);
|
||||
--drawer-soft: var(--wire-color-info-soft);
|
||||
}
|
||||
|
||||
.wire-drawer[data-color="success"] {
|
||||
--drawer-accent: var(--wire-color-success);
|
||||
--drawer-soft: var(--wire-color-success-soft);
|
||||
}
|
||||
|
||||
.wire-drawer[data-color="warning"] {
|
||||
--drawer-accent: var(--wire-color-warning);
|
||||
--drawer-soft: var(--wire-color-warning-soft);
|
||||
}
|
||||
|
||||
.wire-drawer[data-color="danger"] {
|
||||
--drawer-accent: var(--wire-color-danger);
|
||||
--drawer-soft: var(--wire-color-danger-soft);
|
||||
}
|
||||
|
||||
.wire-drawer__trigger,
|
||||
.wire-drawer__trigger-slot {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
.wire-drawer__trigger {
|
||||
appearance: none;
|
||||
min-height: 2.55rem;
|
||||
padding: 0.65rem 1rem;
|
||||
color: var(--wire-color-primary-contrast);
|
||||
background: var(--drawer-accent);
|
||||
border: 0;
|
||||
border-radius: 0.8rem;
|
||||
font: inherit;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 650;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wire-drawer__layer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1200;
|
||||
display: flex;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wire-drawer__backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
appearance: none;
|
||||
padding: 0;
|
||||
background: color-mix(in srgb, black 56%, transparent);
|
||||
border: 0;
|
||||
backdrop-filter: blur(7px);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.wire-drawer[data-overlay="false"] .wire-drawer__backdrop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wire-drawer__panel {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: min(28rem, calc(100vw - 1rem));
|
||||
max-height: 100%;
|
||||
margin-left: auto;
|
||||
color: var(--wire-color-text);
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 100% 0%,
|
||||
color-mix(in srgb, var(--drawer-accent) 8%, transparent),
|
||||
transparent 34%
|
||||
),
|
||||
var(--wire-color-surface-raised);
|
||||
border-left: 1px solid color-mix(in srgb, var(--drawer-accent) 18%, var(--wire-color-border));
|
||||
box-shadow: -28px 0 80px color-mix(in srgb, black 28%, transparent);
|
||||
pointer-events: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wire-drawer[data-size="sm"] .wire-drawer__panel {
|
||||
width: min(22rem, calc(100vw - 1rem));
|
||||
}
|
||||
|
||||
.wire-drawer[data-size="lg"] .wire-drawer__panel {
|
||||
width: min(38rem, calc(100vw - 1rem));
|
||||
}
|
||||
|
||||
.wire-drawer[data-size="xl"] .wire-drawer__panel {
|
||||
width: min(52rem, calc(100vw - 1rem));
|
||||
}
|
||||
|
||||
.wire-drawer[data-size="full"] .wire-drawer__panel {
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.wire-drawer[data-placement="left"] .wire-drawer__panel {
|
||||
margin-right: auto;
|
||||
margin-left: 0;
|
||||
border-right: 1px solid color-mix(in srgb, var(--drawer-accent) 18%, var(--wire-color-border));
|
||||
border-left: 0;
|
||||
box-shadow: 28px 0 80px color-mix(in srgb, black 28%, transparent);
|
||||
}
|
||||
|
||||
.wire-drawer[data-placement="top"] .wire-drawer__layer,
|
||||
.wire-drawer[data-placement="bottom"] .wire-drawer__layer {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.wire-drawer[data-placement="top"] .wire-drawer__panel,
|
||||
.wire-drawer[data-placement="bottom"] .wire-drawer__panel {
|
||||
width: 100%;
|
||||
max-height: min(80vh, 44rem);
|
||||
margin: 0;
|
||||
border: 0;
|
||||
box-shadow: 0 24px 80px color-mix(in srgb, black 28%, transparent);
|
||||
}
|
||||
|
||||
.wire-drawer[data-placement="top"] .wire-drawer__panel {
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--drawer-accent) 18%, var(--wire-color-border));
|
||||
}
|
||||
|
||||
.wire-drawer[data-placement="bottom"] .wire-drawer__layer {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.wire-drawer[data-placement="bottom"] .wire-drawer__panel {
|
||||
border-top: 1px solid color-mix(in srgb, var(--drawer-accent) 18%, var(--wire-color-border));
|
||||
border-radius: 1.3rem 1.3rem 0 0;
|
||||
}
|
||||
|
||||
.wire-drawer[data-variant="soft"] .wire-drawer__panel {
|
||||
background:
|
||||
linear-gradient(145deg, var(--drawer-soft), transparent 65%),
|
||||
var(--wire-color-surface-raised);
|
||||
}
|
||||
|
||||
.wire-drawer[data-variant="solid"] .wire-drawer__panel {
|
||||
color: var(--wire-color-primary-contrast);
|
||||
background: var(--drawer-accent);
|
||||
border-color: color-mix(in srgb, white 18%, transparent);
|
||||
}
|
||||
|
||||
.wire-drawer__handle {
|
||||
display: none;
|
||||
width: 2.75rem;
|
||||
height: 0.28rem;
|
||||
margin: 0.55rem auto 0;
|
||||
background: color-mix(in srgb, var(--wire-color-text-muted) 48%, transparent);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.wire-drawer[data-placement="bottom"] .wire-drawer__handle {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.wire-drawer__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1.35rem 1.35rem 1.1rem;
|
||||
border-bottom: 1px solid var(--wire-color-border);
|
||||
}
|
||||
|
||||
.wire-drawer__heading {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.85rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-drawer__icon {
|
||||
flex: 0 0 auto;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-top: 0.15rem;
|
||||
color: var(--drawer-accent);
|
||||
}
|
||||
|
||||
.wire-drawer[data-variant="solid"] .wire-drawer__icon {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.wire-drawer__heading-copy {
|
||||
display: grid;
|
||||
gap: 0.3rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-drawer__heading-copy h2,
|
||||
.wire-drawer__heading-copy p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wire-drawer__heading-copy h2 {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 650;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.wire-drawer__heading-copy p {
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.wire-drawer[data-variant="solid"] .wire-drawer__heading-copy p {
|
||||
color: color-mix(in srgb, currentColor 76%, transparent);
|
||||
}
|
||||
|
||||
.wire-drawer__close {
|
||||
appearance: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
width: 2.35rem;
|
||||
height: 2.35rem;
|
||||
color: var(--wire-color-text-muted);
|
||||
background: var(--wire-color-surface-soft);
|
||||
border: 1px solid var(--wire-color-border);
|
||||
border-radius: 0.75rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wire-drawer__close:hover,
|
||||
.wire-drawer__close:focus-visible {
|
||||
color: var(--drawer-accent);
|
||||
border-color: color-mix(in srgb, var(--drawer-accent) 34%, var(--wire-color-border));
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.wire-drawer__body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
padding: 1.35rem;
|
||||
}
|
||||
|
||||
.wire-drawer[data-scrollable="true"] .wire-drawer__body {
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.wire-drawer__footer {
|
||||
padding: 1rem 1.35rem 1.35rem;
|
||||
border-top: 1px solid var(--wire-color-border);
|
||||
}
|
||||
|
||||
.wire-drawer__footer:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.wire-drawer[data-placement="right"] .wire-drawer__panel,
|
||||
.wire-drawer[data-placement="left"] .wire-drawer__panel {
|
||||
width: min(24rem, 100vw);
|
||||
}
|
||||
|
||||
.wire-drawer__header,
|
||||
.wire-drawer__body,
|
||||
.wire-drawer__footer {
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,572 @@
|
||||
component Dropdown {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Dropdown"
|
||||
description = ""
|
||||
open = false
|
||||
placement = "bottom"
|
||||
closeLabel = "Close"
|
||||
class = ""
|
||||
outputs {
|
||||
toggle(payload: { open: boolean; sourceEvent: Event; reason?: object })
|
||||
open(payload: { sourceEvent: Event })
|
||||
close(payload: { reason: string; sourceEvent: Event })
|
||||
select(payload: { item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })
|
||||
action(payload: { item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })
|
||||
}
|
||||
|
||||
props {
|
||||
items: unknown[] = []
|
||||
open: boolean = false
|
||||
defaultOpen: boolean = false
|
||||
label: string = "Open menu"
|
||||
icon: string = ""
|
||||
showChevron: boolean = true
|
||||
menuLabel: string = "Dropdown menu"
|
||||
placement: string = "bottom-start"
|
||||
width: string = "md"
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
variant: string = "raised"
|
||||
closeOnSelect: boolean = true
|
||||
closeOnOutside: boolean = true
|
||||
disabled: boolean = false
|
||||
emptyLabel: string = "No menu items"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
state visible = defaultOpen
|
||||
|
||||
functions {
|
||||
shared function isOpen() {
|
||||
return open || visible
|
||||
}
|
||||
|
||||
client function showMenu(sourceEvent) {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
visible = true
|
||||
output.open({ sourceEvent: sourceEvent })
|
||||
output.toggle({ open: true, sourceEvent: sourceEvent })
|
||||
}
|
||||
|
||||
client function hideMenu(reason, sourceEvent) {
|
||||
visible = false
|
||||
output.close({ reason: reason, sourceEvent: sourceEvent })
|
||||
output.toggle({ open: false, reason: reason, sourceEvent: sourceEvent })
|
||||
}
|
||||
|
||||
client function toggleMenu(sourceEvent) {
|
||||
if (isOpen()) {
|
||||
hideMenu("toggle", sourceEvent)
|
||||
} else {
|
||||
showMenu(sourceEvent)
|
||||
}
|
||||
}
|
||||
|
||||
client function chooseItem(item, itemIndex, sourceEvent) {
|
||||
if (disabled || item.disabled || item.type === "divider" || item.type === "header") {
|
||||
sourceEvent.preventDefault()
|
||||
return
|
||||
}
|
||||
output.select({
|
||||
item: item,
|
||||
itemIndex: itemIndex,
|
||||
value: item.value || "",
|
||||
sourceEvent: sourceEvent
|
||||
})
|
||||
if (item.action) {
|
||||
output.action({
|
||||
item: item,
|
||||
itemIndex: itemIndex,
|
||||
value: item.value || "",
|
||||
sourceEvent: sourceEvent
|
||||
})
|
||||
}
|
||||
if (closeOnSelect) {
|
||||
hideMenu("select", sourceEvent)
|
||||
}
|
||||
}
|
||||
|
||||
client function moveFocus(sourceEvent, direction) {
|
||||
const root = sourceEvent.currentTarget.closest(".wire-dropdown") || sourceEvent.currentTarget
|
||||
const options = [...root.querySelectorAll("[data-dropdown-item]:not([disabled])")]
|
||||
if (!options.length) {
|
||||
return
|
||||
}
|
||||
const activeIndex = options.indexOf(document.activeElement)
|
||||
const nextIndex = (activeIndex + direction + options.length) % options.length
|
||||
options[nextIndex].focus()
|
||||
}
|
||||
|
||||
client function handleKeydown(sourceEvent) {
|
||||
if (sourceEvent.key === "Escape") {
|
||||
sourceEvent.preventDefault()
|
||||
hideMenu("escape", sourceEvent)
|
||||
} else if (sourceEvent.key === "ArrowDown") {
|
||||
sourceEvent.preventDefault()
|
||||
if (!isOpen()) {
|
||||
showMenu(sourceEvent)
|
||||
}
|
||||
moveFocus(sourceEvent, 1)
|
||||
} else if (sourceEvent.key === "ArrowUp") {
|
||||
sourceEvent.preventDefault()
|
||||
if (!isOpen()) {
|
||||
showMenu(sourceEvent)
|
||||
}
|
||||
moveFocus(sourceEvent, -1)
|
||||
} else if (sourceEvent.key === "Home" && isOpen()) {
|
||||
sourceEvent.preventDefault()
|
||||
const root = sourceEvent.currentTarget.closest(".wire-dropdown") || sourceEvent.currentTarget
|
||||
const first = root.querySelector("[data-dropdown-item]:not([disabled])")
|
||||
if (first) {
|
||||
first.focus()
|
||||
}
|
||||
} else if (sourceEvent.key === "End" && isOpen()) {
|
||||
sourceEvent.preventDefault()
|
||||
const root = sourceEvent.currentTarget.closest(".wire-dropdown") || sourceEvent.currentTarget
|
||||
const options = [...root.querySelectorAll("[data-dropdown-item]:not([disabled])")]
|
||||
if (options.length) {
|
||||
options[options.length - 1].focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<div data-show="{open}" class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--dropdown wire-next--placement-{placement} {class}" role="region" aria-modal="false" aria-label="{title}">
|
||||
<header><strong>{title}</strong><button type="button" aria-label="{closeLabel}">×</button></header>
|
||||
{#if description}<p>{description}</p>{/if}
|
||||
<slot />
|
||||
<div
|
||||
{...attrs}
|
||||
data-ui-component="Dropdown"
|
||||
data-open='{open || visible ? "true" : "false"}'
|
||||
data-placement='{placement}'
|
||||
data-width='{width}'
|
||||
data-size='{size}'
|
||||
data-color='{color}'
|
||||
data-variant='{variant}'
|
||||
class='wire-dropdown {class}'
|
||||
@keydown='handleKeydown(event)'
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="wire-dropdown__trigger"
|
||||
disabled='{disabled}'
|
||||
aria-haspopup="menu"
|
||||
aria-expanded='{open || visible ? "true" : "false"}'
|
||||
@click='toggleMenu(event)'
|
||||
>
|
||||
<slot name="trigger"></slot>
|
||||
{#if icon}
|
||||
<span class='wire-dropdown__trigger-icon {icon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
{#if label}
|
||||
<span class="wire-dropdown__trigger-label">{label}</span>
|
||||
{/if}
|
||||
{#if showChevron}
|
||||
<span class="icon-[lucide--chevron-down] wire-dropdown__chevron" aria-hidden="true"></span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if closeOnOutside}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-dropdown__dismiss-layer"
|
||||
data-show='{open || visible}'
|
||||
aria-label="Close menu"
|
||||
@click='hideMenu("outside", event)'
|
||||
></button>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="wire-dropdown__panel"
|
||||
data-show='{open || visible}'
|
||||
role="menu"
|
||||
aria-label='{menuLabel}'
|
||||
>
|
||||
<slot name="header"></slot>
|
||||
|
||||
<div class="wire-dropdown__items">
|
||||
{#each items as item, itemIndex}
|
||||
{#if item.type === "divider"}
|
||||
<div class="wire-dropdown__divider" role="separator"></div>
|
||||
{:else if item.type === "header"}
|
||||
<div class="wire-dropdown__section-label">{item.label || item.title}</div>
|
||||
{:else if item.href}
|
||||
<a
|
||||
href='{item.href}'
|
||||
target='{item.target || ""}'
|
||||
rel='{item.external || item.target === "_blank" ? "noopener noreferrer" : (item.rel || "")}'
|
||||
role="menuitem"
|
||||
data-dropdown-item
|
||||
data-danger='{item.danger ? "true" : "false"}'
|
||||
data-selected='{item.selected || item.checked ? "true" : "false"}'
|
||||
aria-disabled='{item.disabled ? "true" : "false"}'
|
||||
class="wire-dropdown__item"
|
||||
@click='chooseItem(item, itemIndex, event)'
|
||||
>
|
||||
{#if item.icon}
|
||||
<span class='wire-dropdown__item-icon {item.icon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span class="wire-dropdown__copy">
|
||||
<strong>{item.label || item.title}</strong>
|
||||
{#if item.description}
|
||||
<small>{item.description}</small>
|
||||
{/if}
|
||||
</span>
|
||||
{#if item.badge}
|
||||
<span class="wire-dropdown__badge">{item.badge}</span>
|
||||
{:else if item.shortcut}
|
||||
<kbd>{item.shortcut}</kbd>
|
||||
{:else if item.checked || item.selected}
|
||||
<span class="icon-[lucide--check] wire-dropdown__status" aria-hidden="true"></span>
|
||||
{:else if item.external}
|
||||
<span class="icon-[lucide--arrow-up-right] wire-dropdown__status" aria-hidden="true"></span>
|
||||
{/if}
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
data-dropdown-item
|
||||
data-danger='{item.danger ? "true" : "false"}'
|
||||
data-selected='{item.selected || item.checked ? "true" : "false"}'
|
||||
disabled='{item.disabled}'
|
||||
class="wire-dropdown__item"
|
||||
@click='chooseItem(item, itemIndex, event)'
|
||||
>
|
||||
{#if item.icon}
|
||||
<span class='wire-dropdown__item-icon {item.icon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span class="wire-dropdown__copy">
|
||||
<strong>{item.label || item.title}</strong>
|
||||
{#if item.description}
|
||||
<small>{item.description}</small>
|
||||
{/if}
|
||||
</span>
|
||||
{#if item.badge}
|
||||
<span class="wire-dropdown__badge">{item.badge}</span>
|
||||
{:else if item.shortcut}
|
||||
<kbd>{item.shortcut}</kbd>
|
||||
{:else if item.checked || item.selected}
|
||||
<span class="icon-[lucide--check] wire-dropdown__status" aria-hidden="true"></span>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
{:empty}
|
||||
<div class="wire-dropdown__empty">{emptyLabel}</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<slot></slot>
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-dropdown {
|
||||
--dropdown-accent: var(--wire-color-primary);
|
||||
--dropdown-soft: var(--wire-color-primary-soft);
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-dropdown[data-color="secondary"] {
|
||||
--dropdown-accent: var(--wire-color-secondary);
|
||||
--dropdown-soft: var(--wire-color-secondary-soft);
|
||||
}
|
||||
|
||||
.wire-dropdown[data-color="info"] {
|
||||
--dropdown-accent: var(--wire-color-info);
|
||||
--dropdown-soft: var(--wire-color-info-soft);
|
||||
}
|
||||
|
||||
.wire-dropdown[data-color="success"] {
|
||||
--dropdown-accent: var(--wire-color-success);
|
||||
--dropdown-soft: var(--wire-color-success-soft);
|
||||
}
|
||||
|
||||
.wire-dropdown[data-color="warning"] {
|
||||
--dropdown-accent: var(--wire-color-warning);
|
||||
--dropdown-soft: var(--wire-color-warning-soft);
|
||||
}
|
||||
|
||||
.wire-dropdown[data-color="danger"] {
|
||||
--dropdown-accent: var(--wire-color-danger);
|
||||
--dropdown-soft: var(--wire-color-danger-soft);
|
||||
}
|
||||
|
||||
.wire-dropdown__trigger {
|
||||
appearance: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.55rem;
|
||||
min-height: 2.55rem;
|
||||
padding: 0.65rem 0.9rem;
|
||||
color: var(--wire-color-text);
|
||||
background: var(--wire-color-surface-raised);
|
||||
border: 1px solid var(--wire-color-border);
|
||||
border-radius: 0.78rem;
|
||||
font: inherit;
|
||||
font-size: 0.84rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: border-color 150ms ease, background-color 150ms ease, color 150ms ease;
|
||||
}
|
||||
|
||||
.wire-dropdown__trigger:hover,
|
||||
.wire-dropdown__trigger:focus-visible,
|
||||
.wire-dropdown[data-open="true"] .wire-dropdown__trigger {
|
||||
color: var(--dropdown-accent);
|
||||
background: var(--dropdown-soft);
|
||||
border-color: color-mix(in srgb, var(--dropdown-accent) 38%, var(--wire-color-border));
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.wire-dropdown__trigger:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.wire-dropdown__trigger-icon,
|
||||
.wire-dropdown__chevron {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.wire-dropdown__chevron {
|
||||
transition: transform 160ms ease;
|
||||
}
|
||||
|
||||
.wire-dropdown[data-open="true"] .wire-dropdown__chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.wire-dropdown[data-size="sm"] .wire-dropdown__trigger {
|
||||
min-height: 2.2rem;
|
||||
padding: 0.5rem 0.72rem;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.wire-dropdown[data-size="lg"] .wire-dropdown__trigger {
|
||||
min-height: 2.9rem;
|
||||
padding: 0.78rem 1.05rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.wire-dropdown__dismiss-layer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1100;
|
||||
appearance: none;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.wire-dropdown__panel {
|
||||
position: absolute;
|
||||
z-index: 1101;
|
||||
top: calc(100% + 0.55rem);
|
||||
left: 0;
|
||||
width: max-content;
|
||||
min-width: 12rem;
|
||||
max-width: min(22rem, calc(100vw - 1.5rem));
|
||||
padding: 0.45rem;
|
||||
color: var(--wire-color-text);
|
||||
background:
|
||||
linear-gradient(145deg, color-mix(in srgb, var(--dropdown-accent) 5%, transparent), transparent 60%),
|
||||
color-mix(in srgb, var(--wire-color-surface-raised) 97%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--dropdown-accent) 18%, var(--wire-color-border));
|
||||
border-radius: 1rem;
|
||||
box-shadow:
|
||||
0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
|
||||
0 24px 64px color-mix(in srgb, black 22%, transparent);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.wire-dropdown[data-width="trigger"] .wire-dropdown__panel {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.wire-dropdown[data-width="sm"] .wire-dropdown__panel {
|
||||
width: 12rem;
|
||||
}
|
||||
|
||||
.wire-dropdown[data-width="md"] .wire-dropdown__panel {
|
||||
width: 17rem;
|
||||
}
|
||||
|
||||
.wire-dropdown[data-width="lg"] .wire-dropdown__panel {
|
||||
width: 22rem;
|
||||
}
|
||||
|
||||
.wire-dropdown[data-placement="bottom-end"] .wire-dropdown__panel {
|
||||
right: 0;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.wire-dropdown[data-placement="top-start"] .wire-dropdown__panel {
|
||||
top: auto;
|
||||
bottom: calc(100% + 0.55rem);
|
||||
}
|
||||
|
||||
.wire-dropdown[data-placement="top-end"] .wire-dropdown__panel {
|
||||
top: auto;
|
||||
right: 0;
|
||||
bottom: calc(100% + 0.55rem);
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.wire-dropdown[data-variant="soft"] .wire-dropdown__panel {
|
||||
background:
|
||||
linear-gradient(145deg, var(--dropdown-soft), transparent 70%),
|
||||
var(--wire-color-surface-raised);
|
||||
box-shadow: 0 18px 48px color-mix(in srgb, black 16%, transparent);
|
||||
}
|
||||
|
||||
.wire-dropdown[data-variant="outline"] .wire-dropdown__panel {
|
||||
background: var(--wire-color-surface-raised);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wire-dropdown__items {
|
||||
display: grid;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.wire-dropdown__section-label {
|
||||
padding: 0.55rem 0.72rem 0.3rem;
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.wire-dropdown__divider {
|
||||
height: 1px;
|
||||
margin: 0.3rem 0.4rem;
|
||||
background: var(--wire-color-border);
|
||||
}
|
||||
|
||||
.wire-dropdown__item {
|
||||
appearance: none;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 0.7rem;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 0.68rem 0.72rem;
|
||||
color: var(--wire-color-text);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 0.72rem;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 150ms ease, color 150ms ease, transform 150ms ease;
|
||||
}
|
||||
|
||||
.wire-dropdown__item:hover,
|
||||
.wire-dropdown__item:focus-visible,
|
||||
.wire-dropdown__item[data-selected="true"] {
|
||||
color: var(--dropdown-accent);
|
||||
background: var(--dropdown-soft);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.wire-dropdown__item:active {
|
||||
transform: scale(0.985);
|
||||
}
|
||||
|
||||
.wire-dropdown__item[data-danger="true"] {
|
||||
color: var(--wire-color-danger);
|
||||
}
|
||||
|
||||
.wire-dropdown__item[data-danger="true"]:hover,
|
||||
.wire-dropdown__item[data-danger="true"]:focus-visible {
|
||||
background: var(--wire-color-danger-soft);
|
||||
}
|
||||
|
||||
.wire-dropdown__item[disabled],
|
||||
.wire-dropdown__item[aria-disabled="true"] {
|
||||
opacity: 0.48;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wire-dropdown__item-icon,
|
||||
.wire-dropdown__status {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.wire-dropdown__copy {
|
||||
display: grid;
|
||||
gap: 0.12rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-dropdown__copy strong {
|
||||
overflow: hidden;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wire-dropdown__copy small {
|
||||
overflow: hidden;
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.35;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wire-dropdown__badge {
|
||||
padding: 0.18rem 0.45rem;
|
||||
color: var(--dropdown-accent);
|
||||
background: var(--dropdown-soft);
|
||||
border-radius: 999px;
|
||||
font-size: 0.64rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.wire-dropdown kbd {
|
||||
padding: 0.16rem 0.38rem;
|
||||
color: var(--wire-color-text-muted);
|
||||
background: var(--wire-color-surface-soft);
|
||||
border: 1px solid var(--wire-color-border);
|
||||
border-radius: 0.36rem;
|
||||
font-size: 0.64rem;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.wire-dropdown__empty {
|
||||
padding: 1rem;
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.8rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.wire-dropdown__panel {
|
||||
position: fixed;
|
||||
right: 0.75rem;
|
||||
bottom: 0.75rem;
|
||||
left: 0.75rem;
|
||||
top: auto;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wire-dropdown__trigger,
|
||||
.wire-dropdown__chevron,
|
||||
.wire-dropdown__item {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,55 @@
|
||||
component FileInput {
|
||||
outputs {
|
||||
input(payload: { files: File[]; name: string; sourceEvent: Event })
|
||||
change(payload: { files: File[]; name: string; sourceEvent: Event })
|
||||
focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
select(payload: { files: File[]; name: string; sourceEvent: Event })
|
||||
clear(payload: { name: string; sourceEvent: Event })
|
||||
invalid(payload: { message?: string; value: string | number | boolean | null | object; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "File Input"
|
||||
name = ""
|
||||
value = ""
|
||||
placeholder = ""
|
||||
type = "file"
|
||||
min = ""
|
||||
max = ""
|
||||
step = ""
|
||||
disabled = false
|
||||
required = false
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
id: string = ""
|
||||
name: string = ""
|
||||
label: string = "File"
|
||||
hiddenLabel: boolean = false
|
||||
placeholder: string = "Choose a file"
|
||||
value: string = ""
|
||||
icon: string = "icon-[lucide--upload]"
|
||||
iconPosition: string = "start"
|
||||
accept: string = ""
|
||||
multiple: boolean = false
|
||||
helperText: string = ""
|
||||
cornerHint: string = ""
|
||||
error: string = ""
|
||||
inline: boolean = false
|
||||
variant: string = "normal"
|
||||
readonly: boolean = false
|
||||
disabled: boolean = false
|
||||
required: boolean = false
|
||||
class: string = ""
|
||||
}
|
||||
state selectedName: string = ""
|
||||
functions {
|
||||
client function handleChange(sourceEvent) {
|
||||
sourceEvent.stopPropagation()
|
||||
selectedName = sourceEvent.currentTarget.files && sourceEvent.currentTarget.files.length ? sourceEvent.currentTarget.files[0].name : ""
|
||||
output.input({ files: sourceEvent.currentTarget.files, name: name, sourceEvent: sourceEvent })
|
||||
output.change({ files: sourceEvent.currentTarget.files, name: name, sourceEvent: sourceEvent })
|
||||
if (selectedName) output.select({ files: sourceEvent.currentTarget.files, name: name, sourceEvent: sourceEvent })
|
||||
else output.clear({ name: name, sourceEvent: sourceEvent })
|
||||
}
|
||||
client function emitField(nameEvent, sourceEvent) { sourceEvent.stopPropagation(); output[nameEvent]({ name: name, sourceEvent: sourceEvent }) }
|
||||
client function preventReadonly(sourceEvent) { if (readonly) sourceEvent.preventDefault() }
|
||||
}
|
||||
view {
|
||||
<label class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--file-input {class}"><span>{label}</span><input {...attrs} type="{type}" name="{name}" value="{value}" placeholder="{placeholder}" min="{min}" max="{max}" step="{step}" disabled="{disabled}" required="{required}" /></label>
|
||||
<div {...attrs} class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--file-input {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}">
|
||||
<div class="wire-next__field-heading"><label class="{hiddenLabel ? 'wire-next__sr-only' : ''}" for="{id || name}">{label}</label>{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}</div>
|
||||
<div class="wire-next__file-control" data-icon-position="{iconPosition}">{#if icon}<span class="{icon}" aria-hidden="true"></span>{/if}<span>{selectedName || value || placeholder}</span><input id="{id || name}" type="file" name="{name}" accept="{accept}" multiple="{multiple}" disabled="{disabled}" required="{required}" aria-readonly="{readonly}" aria-invalid="{error ? 'true' : 'false'}" @click="preventReadonly(event)" @input="handleChange(event)" @change="handleChange(event)" @focus="emitField('focus', event)" @blur="emitField('blur', event)" @invalid="emitField('invalid', event)" /></div>
|
||||
{#if helperText}<small class="wire-next__field-help">{helperText}</small>{/if}<small class="wire-next__field-error" data-error="{name}">{error}</small>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
component FileUpload {
|
||||
outputs {
|
||||
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
upload(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
progress(payload: { progress: number; [key: string]: string | number | boolean | null | object } | number)
|
||||
success(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
error(payload: { error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)
|
||||
cancel(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
remove(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "File Upload"
|
||||
description = ""
|
||||
items = []
|
||||
variant = "default"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
title: string = "File Upload"
|
||||
description: string = ""
|
||||
items: unknown[] = []
|
||||
variant: string = "default"
|
||||
class: string = ""
|
||||
}
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--file-upload wire-next--variant-{variant} {class}">
|
||||
|
||||
@@ -1,17 +1,40 @@
|
||||
component FileUploadProgress {
|
||||
outputs {
|
||||
cancel(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
|
||||
retry(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
|
||||
complete(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "Progress"
|
||||
value = 50
|
||||
max = 100
|
||||
showValue = true
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
label: string = "Progress"
|
||||
value: number = 50
|
||||
max: number = 100
|
||||
showValue: boolean = true
|
||||
fileName: string = ""
|
||||
fileSize: string = ""
|
||||
uploadedSize: string = ""
|
||||
status: string = "uploading"
|
||||
cancelLabel: string = "Cancel upload"
|
||||
retryLabel: string = "Retry upload"
|
||||
class: string = ""
|
||||
}
|
||||
state currentValue = value
|
||||
state currentStatus = status
|
||||
functions {
|
||||
shared function percent() { if (!Number(max)) { return 0 } return Math.max(0, Math.min(100, Math.round(Number(currentValue) / Number(max) * 100))) }
|
||||
client function detail(sourceEvent) { return { value: currentValue, max: max, percent: percent(), status: currentStatus, fileName: fileName, sourceEvent: sourceEvent } }
|
||||
client function cancelUpload(sourceEvent) { currentStatus = "cancelled"; output.cancel(detail(sourceEvent)) }
|
||||
client function retryUpload(sourceEvent) { currentValue = 0; currentStatus = "uploading"; output.retry(detail(sourceEvent)) }
|
||||
client function completeUpload(sourceEvent) { currentValue = max; currentStatus = "complete"; output.complete(detail(sourceEvent)) }
|
||||
}
|
||||
view {
|
||||
<div class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--file-upload-progress {class}">
|
||||
<div class="wire-next__row"><span>{label}</span>{#if showValue}<strong>{value}%</strong>{/if}</div>
|
||||
<progress value="{value}" max="{max}"></progress>
|
||||
<div {...attrs} class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--file-upload-progress {class}" data-status="{currentStatus}" aria-live="polite">
|
||||
<div class="wire-next__row"><span>{#if fileName}<strong>{fileName}</strong><small>{#if uploadedSize || fileSize}{uploadedSize}{#if uploadedSize && fileSize} of {/if}{fileSize}{:else}{label}{/if}</small>{:else}<strong>{label}</strong>{/if}</span>{#if showValue}<strong>{percent()}%</strong>{/if}</div>
|
||||
<progress value="{currentValue}" max="{max}" aria-label="{label}"></progress>
|
||||
<div class="wire-next__upload-status"><span class="wire-next__upload-status-icon" aria-hidden="true"></span><span>{#if currentStatus === "uploading"}Uploading securely…{:else if currentStatus === "complete"}Upload complete{:else if currentStatus === "error"}Upload failed{:else}Upload cancelled{/if}</span></div>
|
||||
<div class="wire-next__upload-actions">{#if currentStatus === "uploading"}<button type="button" @click="cancelUpload(event)">{cancelLabel}</button><button type="button" @click="completeUpload(event)">Complete</button>{/if}{#if currentStatus === "error" || currentStatus === "cancelled"}<button type="button" @click="retryUpload(event)">{retryLabel}</button>{/if}</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,47 @@
|
||||
component Image {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
src = ""
|
||||
alt = ""
|
||||
width = ""
|
||||
height = ""
|
||||
loading = "lazy"
|
||||
rounded = false
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
src: string = ""
|
||||
alt: string = ""
|
||||
width: string = ""
|
||||
height: string = ""
|
||||
loading: string = "lazy"
|
||||
rounded: boolean = false
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
view {
|
||||
<figure class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--image {rounded ? 'wire-next--rounded' : ''} {class}">
|
||||
<img src="{src}" alt="{alt}" width="{width}" height="{height}" loading="{loading}" />
|
||||
<slot />
|
||||
<figure
|
||||
data-ui-component="Image"
|
||||
class='relative m-0 overflow-hidden bg-[var(--wire-color-surface-soft)] {class}'
|
||||
class:rounded-2xl='rounded'
|
||||
>
|
||||
{#if src}
|
||||
<img
|
||||
src='{src}'
|
||||
alt='{alt}'
|
||||
width='{width}'
|
||||
height='{height}'
|
||||
loading='{loading}'
|
||||
decoding="async"
|
||||
class="block h-auto w-full object-cover transition duration-300"
|
||||
class:aspect-square='size === "square"'
|
||||
class:aspect-video='size === "video"'
|
||||
class:aspect-[4/3]='size === "landscape"'
|
||||
class:aspect-[3/4]='size === "portrait"'
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class="flex min-h-48 w-full items-center justify-center text-[var(--wire-color-text-muted)]"
|
||||
role="img"
|
||||
aria-label='{alt || "Image placeholder"}'
|
||||
>
|
||||
<span class="icon-[lucide--image] size-8" aria-hidden="true"></span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<slot></slot>
|
||||
</figure>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,54 @@
|
||||
component InputGroup {
|
||||
outputs {
|
||||
input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
submit(payload: { value: string | number | boolean | null | object; name: string; sourceEvent: Event })
|
||||
action(payload: { name: string; sourceEvent: Event })
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "Input Group"
|
||||
name = ""
|
||||
value = ""
|
||||
placeholder = ""
|
||||
type = "text"
|
||||
min = ""
|
||||
max = ""
|
||||
step = ""
|
||||
disabled = false
|
||||
required = false
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
id: string = ""
|
||||
name: string = ""
|
||||
label: string = "Input group"
|
||||
hiddenLabel: boolean = false
|
||||
value: string = ""
|
||||
placeholder: string = ""
|
||||
type: string = "text"
|
||||
startText: string = ""
|
||||
endText: string = ""
|
||||
icon: string = ""
|
||||
iconPosition: string = "start"
|
||||
actionLabel: string = ""
|
||||
helperText: string = ""
|
||||
cornerHint: string = ""
|
||||
error: string = ""
|
||||
inline: boolean = false
|
||||
variant: string = "normal"
|
||||
readonly: boolean = false
|
||||
disabled: boolean = false
|
||||
required: boolean = false
|
||||
class: string = ""
|
||||
}
|
||||
functions {
|
||||
client function emitField(nameEvent, sourceEvent) { sourceEvent.stopPropagation(); output[nameEvent]({ value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent }) }
|
||||
client function handleKeydown(sourceEvent) { if (sourceEvent.key === "Enter") { sourceEvent.stopPropagation(); output.submit({ value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent }) } }
|
||||
client function handleAction(sourceEvent) { output.action({ name: name, sourceEvent: sourceEvent }) }
|
||||
}
|
||||
view {
|
||||
<label class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--input-group {class}"><span>{label}</span><input {...attrs} type="{type}" name="{name}" value="{value}" placeholder="{placeholder}" min="{min}" max="{max}" step="{step}" disabled="{disabled}" required="{required}" /></label>
|
||||
<div {...attrs} class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--input-group {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}">
|
||||
<div class="wire-next__field-heading"><label class="{hiddenLabel ? 'wire-next__sr-only' : ''}" for="{id || name}">{label}</label>{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}</div>
|
||||
<div class="wire-next__input-group-control">
|
||||
{#if startText}<span class="wire-next__input-addon">{startText}</span>{/if}
|
||||
{#if icon}<span class="{icon} wire-next__field-icon" data-position="{iconPosition}" aria-hidden="true"></span>{/if}
|
||||
<input id="{id || name}" type="{type}" name="{name}" value="{value}" placeholder="{placeholder}" readonly="{readonly}" disabled="{disabled}" required="{required}" aria-invalid="{error ? 'true' : 'false'}" @input="emitField('input', event)" @change="emitField('change', event)" @focus="emitField('focus', event)" @blur="emitField('blur', event)" @keydown="handleKeydown(event)" />
|
||||
{#if endText}<span class="wire-next__input-addon">{endText}</span>{/if}
|
||||
{#if actionLabel}<button type="button" disabled="{disabled}" @click="handleAction(event)">{actionLabel}</button>{/if}
|
||||
</div>
|
||||
{#if helperText}<small class="wire-next__field-help">{helperText}</small>{/if}<small class="wire-next__field-error" data-error="{name}">{error}</small>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,626 @@
|
||||
component InputNumber {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "Input Number"
|
||||
name = ""
|
||||
value = ""
|
||||
placeholder = ""
|
||||
type = "number"
|
||||
min = ""
|
||||
max = ""
|
||||
step = ""
|
||||
disabled = false
|
||||
required = false
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<label class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--input-number {class}"><span>{label}</span><input {...attrs} type="{type}" name="{name}" value="{value}" placeholder="{placeholder}" min="{min}" max="{max}" step="{step}" disabled="{disabled}" required="{required}" /></label>
|
||||
}
|
||||
outputs {
|
||||
input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
increment(payload: { value: number; previousValue?: number; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
decrement(payload: { value: number; previousValue?: number; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
}
|
||||
|
||||
props {
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
variant: string = "default"
|
||||
class: string = ""
|
||||
|
||||
id: string = ""
|
||||
name: string = "quantity"
|
||||
value: number = 0
|
||||
min: string = ""
|
||||
max: string = ""
|
||||
step: number = 1
|
||||
precision: string = "auto"
|
||||
|
||||
label: string = ""
|
||||
description: string = ""
|
||||
helpText: string = ""
|
||||
error: string = ""
|
||||
invalid: boolean = false
|
||||
|
||||
prefix: string = ""
|
||||
suffix: string = ""
|
||||
placeholder: string = ""
|
||||
autocomplete: string = "off"
|
||||
inputMode: string = "decimal"
|
||||
ariaLabel: string = ""
|
||||
|
||||
required: boolean = false
|
||||
disabled: boolean = false
|
||||
inputDisabled: boolean = false
|
||||
buttonsDisabled: boolean = false
|
||||
readonly: boolean = false
|
||||
allowInput: boolean = true
|
||||
keyboard: boolean = true
|
||||
wheel: boolean = false
|
||||
clamp: boolean = true
|
||||
fullWidth: boolean = false
|
||||
showButtons: boolean = true
|
||||
showValidationMessage: boolean = true
|
||||
|
||||
decrementLabel: string = "Decrease value"
|
||||
incrementLabel: string = "Increase value"
|
||||
controlsLabel: string = "Quantity controls"
|
||||
requiredMessage: string = "A value is required."
|
||||
minMessage: string = "Value is below the minimum."
|
||||
maxMessage: string = "Value is above the maximum."
|
||||
|
||||
}
|
||||
|
||||
state currentValue = value
|
||||
state committedValue = value
|
||||
|
||||
functions {
|
||||
shared function inputId() {
|
||||
if (id !== "") {
|
||||
return id
|
||||
}
|
||||
|
||||
if (name !== "") {
|
||||
return name
|
||||
}
|
||||
|
||||
return "input-number"
|
||||
}
|
||||
|
||||
shared function descriptionId() {
|
||||
return inputId() + "-description"
|
||||
}
|
||||
|
||||
shared function messageId() {
|
||||
return inputId() + "-message"
|
||||
}
|
||||
|
||||
shared function componentColor() {
|
||||
if (color === "secondary") {
|
||||
return "var(--wire-color-secondary)"
|
||||
}
|
||||
|
||||
if (color === "success") {
|
||||
return "var(--wire-color-success)"
|
||||
}
|
||||
|
||||
if (color === "warning") {
|
||||
return "var(--wire-color-warning)"
|
||||
}
|
||||
|
||||
if (color === "danger") {
|
||||
return "var(--wire-color-danger)"
|
||||
}
|
||||
|
||||
if (color === "info") {
|
||||
return "var(--wire-color-info)"
|
||||
}
|
||||
|
||||
return "var(--wire-color-primary)"
|
||||
}
|
||||
|
||||
shared function hasMin() {
|
||||
return min !== "" && min !== null && min !== undefined
|
||||
}
|
||||
|
||||
shared function hasMax() {
|
||||
return max !== "" && max !== null && max !== undefined
|
||||
}
|
||||
|
||||
shared function isBlank() {
|
||||
return currentValue === "" || currentValue === null || currentValue === undefined
|
||||
}
|
||||
|
||||
shared function normalizedStep() {
|
||||
return Number(step) > 0 ? Number(step) : 1
|
||||
}
|
||||
|
||||
shared function inferredPrecision() {
|
||||
if (precision !== "auto" && precision !== "") {
|
||||
return Math.max(0, Number(precision) || 0)
|
||||
}
|
||||
|
||||
if (String(normalizedStep()).includes(".")) {
|
||||
return String(normalizedStep()).split(".")[1].length
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
shared function precisionFactor() {
|
||||
return Math.pow(10, inferredPrecision())
|
||||
}
|
||||
|
||||
shared function roundValue(nextValue) {
|
||||
return Math.round(Number(nextValue) * precisionFactor()) / precisionFactor()
|
||||
}
|
||||
|
||||
shared function clampValue(nextValue) {
|
||||
nextValue = Number(nextValue)
|
||||
|
||||
if (hasMin() && nextValue < Number(min)) {
|
||||
nextValue = Number(min)
|
||||
}
|
||||
|
||||
if (hasMax() && nextValue > Number(max)) {
|
||||
nextValue = Number(max)
|
||||
}
|
||||
|
||||
return roundValue(nextValue)
|
||||
}
|
||||
|
||||
shared function isBelowMin() {
|
||||
return !isBlank() && hasMin() && Number(currentValue) < Number(min)
|
||||
}
|
||||
|
||||
shared function isAboveMax() {
|
||||
return !isBlank() && hasMax() && Number(currentValue) > Number(max)
|
||||
}
|
||||
|
||||
shared function isInvalid() {
|
||||
return (
|
||||
invalid ||
|
||||
error !== "" ||
|
||||
(required && isBlank()) ||
|
||||
isBelowMin() ||
|
||||
isAboveMax()
|
||||
)
|
||||
}
|
||||
|
||||
shared function validationMessage() {
|
||||
if (error !== "") {
|
||||
return error
|
||||
}
|
||||
|
||||
if (required && isBlank()) {
|
||||
return requiredMessage
|
||||
}
|
||||
|
||||
if (isBelowMin()) {
|
||||
return minMessage
|
||||
}
|
||||
|
||||
if (isAboveMax()) {
|
||||
return maxMessage
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
shared function hasMessage() {
|
||||
return (
|
||||
helpText !== "" ||
|
||||
(showValidationMessage && isInvalid() && validationMessage() !== "")
|
||||
)
|
||||
}
|
||||
|
||||
shared function describedBy() {
|
||||
if (description !== "" && hasMessage()) {
|
||||
return descriptionId() + " " + messageId()
|
||||
}
|
||||
|
||||
if (description !== "") {
|
||||
return descriptionId()
|
||||
}
|
||||
|
||||
if (hasMessage()) {
|
||||
return messageId()
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
shared function decrementDisabled() {
|
||||
return (
|
||||
disabled ||
|
||||
readonly ||
|
||||
buttonsDisabled ||
|
||||
(hasMin() && !isBlank() && Number(currentValue) <= Number(min))
|
||||
)
|
||||
}
|
||||
|
||||
shared function incrementDisabled() {
|
||||
return (
|
||||
disabled ||
|
||||
readonly ||
|
||||
buttonsDisabled ||
|
||||
(hasMax() && !isBlank() && Number(currentValue) >= Number(max))
|
||||
)
|
||||
}
|
||||
|
||||
client function dispatchInputNumberEvent(
|
||||
sourceEvent,
|
||||
eventName,
|
||||
action,
|
||||
previousValue,
|
||||
root,
|
||||
customEvent
|
||||
) {
|
||||
root = sourceEvent.currentTarget.closest("[data-wrn-input-number]")
|
||||
|
||||
if (!root && sourceEvent.target) {
|
||||
root = sourceEvent.target.closest("[data-wrn-input-number]")
|
||||
}
|
||||
|
||||
if (!root) {
|
||||
return
|
||||
}
|
||||
|
||||
customEvent = document.createEvent("CustomEvent")
|
||||
customEvent.initCustomEvent(eventName, true, false, {
|
||||
component: "InputNumber",
|
||||
name: name,
|
||||
value: currentValue,
|
||||
previousValue: previousValue,
|
||||
action: action,
|
||||
min: hasMin() ? Number(min) : null,
|
||||
max: hasMax() ? Number(max) : null,
|
||||
step: normalizedStep(),
|
||||
valid: !isInvalid()
|
||||
})
|
||||
root.dispatchEvent(customEvent)
|
||||
}
|
||||
|
||||
client function applyControlValue(nextValue, action, sourceEvent, previousValue) {
|
||||
previousValue = currentValue
|
||||
currentValue = clampValue(nextValue)
|
||||
committedValue = currentValue
|
||||
|
||||
dispatchInputNumberEvent(
|
||||
sourceEvent,
|
||||
"input",
|
||||
action,
|
||||
previousValue
|
||||
)
|
||||
dispatchInputNumberEvent(
|
||||
sourceEvent,
|
||||
"change",
|
||||
action,
|
||||
previousValue
|
||||
)
|
||||
dispatchInputNumberEvent(
|
||||
sourceEvent,
|
||||
action,
|
||||
action,
|
||||
previousValue
|
||||
)
|
||||
}
|
||||
|
||||
client function incrementValue(sourceEvent, nextValue) {
|
||||
if (incrementDisabled()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isBlank()) {
|
||||
nextValue = hasMin() ? Number(min) : normalizedStep()
|
||||
} else {
|
||||
nextValue =
|
||||
Number(currentValue) +
|
||||
normalizedStep() * (sourceEvent.shiftKey ? 10 : 1)
|
||||
}
|
||||
|
||||
applyControlValue(nextValue, "increment", sourceEvent)
|
||||
}
|
||||
|
||||
client function decrementValue(sourceEvent, nextValue) {
|
||||
if (decrementDisabled()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isBlank()) {
|
||||
nextValue = hasMax() ? Number(max) : -normalizedStep()
|
||||
} else {
|
||||
nextValue =
|
||||
Number(currentValue) -
|
||||
normalizedStep() * (sourceEvent.shiftKey ? 10 : 1)
|
||||
}
|
||||
|
||||
applyControlValue(nextValue, "decrement", sourceEvent)
|
||||
}
|
||||
|
||||
client function handleInput(sourceEvent, previousValue, nextValue) {
|
||||
sourceEvent.stopPropagation()
|
||||
previousValue = currentValue
|
||||
nextValue = sourceEvent.target.value
|
||||
|
||||
if (nextValue === "") {
|
||||
currentValue = ""
|
||||
} else if (!Number.isNaN(Number(nextValue))) {
|
||||
currentValue = roundValue(Number(nextValue))
|
||||
}
|
||||
|
||||
dispatchInputNumberEvent(
|
||||
sourceEvent,
|
||||
"input",
|
||||
"input",
|
||||
previousValue
|
||||
)
|
||||
}
|
||||
|
||||
client function handleChange(sourceEvent, previousValue) {
|
||||
sourceEvent.stopPropagation()
|
||||
previousValue = committedValue
|
||||
|
||||
if (!isBlank()) {
|
||||
currentValue = clamp
|
||||
? clampValue(currentValue)
|
||||
: roundValue(currentValue)
|
||||
}
|
||||
|
||||
committedValue = currentValue
|
||||
|
||||
dispatchInputNumberEvent(
|
||||
sourceEvent,
|
||||
"change",
|
||||
"change",
|
||||
previousValue
|
||||
)
|
||||
}
|
||||
|
||||
client function handleKeydown(sourceEvent) {
|
||||
if (
|
||||
!keyboard ||
|
||||
disabled ||
|
||||
readonly ||
|
||||
inputDisabled ||
|
||||
!allowInput
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (sourceEvent.key === "ArrowUp") {
|
||||
sourceEvent.preventDefault()
|
||||
incrementValue(sourceEvent)
|
||||
} else if (sourceEvent.key === "ArrowDown") {
|
||||
sourceEvent.preventDefault()
|
||||
decrementValue(sourceEvent)
|
||||
} else if (sourceEvent.key === "Home" && hasMin()) {
|
||||
sourceEvent.preventDefault()
|
||||
applyControlValue(Number(min), "decrement", sourceEvent)
|
||||
} else if (sourceEvent.key === "End" && hasMax()) {
|
||||
sourceEvent.preventDefault()
|
||||
applyControlValue(Number(max), "increment", sourceEvent)
|
||||
}
|
||||
}
|
||||
|
||||
client function handleWheel(sourceEvent) {
|
||||
if (
|
||||
!wheel ||
|
||||
disabled ||
|
||||
readonly ||
|
||||
inputDisabled ||
|
||||
!allowInput
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
sourceEvent.preventDefault()
|
||||
|
||||
if (sourceEvent.deltaY < 0) {
|
||||
incrementValue(sourceEvent)
|
||||
} else if (sourceEvent.deltaY > 0) {
|
||||
decrementValue(sourceEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<div
|
||||
{...attrs}
|
||||
data-wrn-input-number
|
||||
data-variant='{variant}'
|
||||
data-size='{size}'
|
||||
data-color='{color}'
|
||||
data-value='{currentValue}'
|
||||
data-invalid='{isInvalid() ? "true" : "false"}'
|
||||
data-disabled='{disabled ? "true" : "false"}'
|
||||
style='--input-number-accent: {componentColor()};'
|
||||
class='relative flex flex-col gap-1.5 text-[var(--wire-color-text)] {fullWidth ? "w-full" : variant === "compact" ? "w-fit max-w-full" : "w-full max-w-sm"} {disabled ? "opacity-60" : ""} {class}'
|
||||
>
|
||||
{#if label !== "" && variant !== "labeled" && variant !== "seat"}
|
||||
<label
|
||||
for='{inputId()}'
|
||||
class='inline-flex items-center gap-1 text-sm font-semibold leading-5 text-[var(--wire-color-text)]'
|
||||
>
|
||||
<span>{label}</span>
|
||||
{#if required}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class='text-[var(--wire-color-danger)]'
|
||||
>*</span>
|
||||
{/if}
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
{#if description !== "" && variant !== "labeled" && variant !== "seat"}
|
||||
<p
|
||||
id='{descriptionId()}'
|
||||
class='m-0 text-xs leading-5 text-[var(--wire-color-muted)]'
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class='group flex min-w-0 overflow-hidden border bg-[var(--wire-color-surface)] text-[var(--wire-color-text)] shadow-[var(--wire-shadow-1)] transition-[border-color,box-shadow,background-color] duration-[var(--wire-motion-base)] ease-[var(--wire-ease-standard)] focus-within:border-[var(--input-number-accent)] focus-within:shadow-[0_0_0_3px_color-mix(in_srgb,var(--input-number-accent)_18%,transparent)] {variant === "compact" ? "rounded-full" : "rounded-[var(--wire-radius-sm)]"} {size === "xs" ? "min-h-8 text-xs" : size === "sm" ? "min-h-9 text-sm" : size === "lg" ? "min-h-12 text-base" : size === "xl" ? "min-h-14 text-lg" : "min-h-10 text-sm"} {isInvalid() ? "border-[var(--wire-color-danger)] focus-within:border-[var(--wire-color-danger)] focus-within:shadow-[0_0_0_3px_color-mix(in_srgb,var(--wire-color-danger)_18%,transparent)]" : "border-[var(--wire-color-border)]"} {disabled ? "cursor-not-allowed bg-[var(--wire-color-surface-2)]" : ""}'
|
||||
>
|
||||
{#if variant === "horizontal" && showButtons}
|
||||
<button
|
||||
type="button"
|
||||
aria-label='{decrementLabel}'
|
||||
aria-controls='{inputId()}'
|
||||
disabled='{decrementDisabled()}'
|
||||
@click='decrementValue(event)'
|
||||
class='inline-flex shrink-0 items-center justify-center border-r border-[var(--wire-color-border)] bg-transparent text-[var(--wire-color-muted)] transition-[color,background-color] duration-[var(--wire-motion-fast)] hover:bg-[var(--wire-color-surface-2)] hover:text-[var(--input-number-accent)] focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--input-number-accent)] disabled:cursor-not-allowed disabled:opacity-40 {size === "xs" ? "w-8" : size === "sm" ? "w-9" : size === "lg" ? "w-12" : size === "xl" ? "w-14" : "w-10"}'
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class='icon-[lucide--minus] size-4'
|
||||
></span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class='flex min-w-0 flex-1 {variant === "labeled" ? "flex-col items-stretch justify-center gap-0.5" : variant === "seat" ? "items-center justify-between gap-3" : "items-center"} {size === "xs" ? "px-2" : size === "sm" ? "px-2.5" : size === "lg" ? "px-4" : size === "xl" ? "px-5" : "px-3"}'
|
||||
>
|
||||
{#if variant === "labeled" || variant === "seat"}
|
||||
<div class='min-w-0 flex-1'>
|
||||
{#if label !== ""}
|
||||
<label
|
||||
for='{inputId()}'
|
||||
class='block truncate font-semibold leading-4 text-[var(--wire-color-text)] {variant === "labeled" ? "text-xs font-medium text-[var(--wire-color-muted)]" : "text-sm"}'
|
||||
>
|
||||
{label}
|
||||
{#if required}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class='ml-0.5 text-[var(--wire-color-danger)]'
|
||||
>*</span>
|
||||
{/if}
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
{#if description !== ""}
|
||||
<span
|
||||
id='{descriptionId()}'
|
||||
class='block truncate text-xs leading-4 text-[var(--wire-color-muted)]'
|
||||
>
|
||||
{description}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class='flex min-w-0 items-center {variant === "seat" ? "w-auto shrink-0" : "w-full"}'
|
||||
>
|
||||
{#if prefix !== ""}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class='shrink-0 pr-1.5 text-[var(--wire-color-muted)]'
|
||||
>
|
||||
{prefix}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<input
|
||||
id='{inputId()}'
|
||||
name='{name}'
|
||||
type="number"
|
||||
value='{currentValue}'
|
||||
min='{min}'
|
||||
max='{max}'
|
||||
step='{normalizedStep()}'
|
||||
placeholder='{placeholder}'
|
||||
autocomplete='{autocomplete}'
|
||||
inputmode='{inputMode}'
|
||||
aria-label='{ariaLabel !== "" ? ariaLabel : label !== "" ? label : name}'
|
||||
aria-describedby='{describedBy()}'
|
||||
aria-invalid='{isInvalid() ? "true" : "false"}'
|
||||
aria-required='{required ? "true" : "false"}'
|
||||
aria-disabled='{disabled || inputDisabled ? "true" : "false"}'
|
||||
required='{required}'
|
||||
disabled='{disabled}'
|
||||
readonly='{readonly || inputDisabled || !allowInput}'
|
||||
tabindex='{inputDisabled ? "-1" : "0"}'
|
||||
@input='handleInput(event)'
|
||||
@change='handleChange(event)'
|
||||
@keydown='handleKeydown(event)'
|
||||
@wheel='handleWheel(event)'
|
||||
class='min-w-0 flex-1 appearance-none border-0 bg-transparent p-0 font-medium leading-none text-[var(--wire-color-text)] outline-none placeholder:text-[var(--wire-color-muted)] read-only:cursor-default disabled:cursor-not-allowed [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none {variant === "horizontal" || variant === "compact" || variant === "seat" ? "text-center" : "text-left"} {variant === "seat" ? "w-10 flex-none" : "w-full"}'
|
||||
/>
|
||||
|
||||
{#if suffix !== ""}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class='shrink-0 pl-1.5 text-[var(--wire-color-muted)]'
|
||||
>
|
||||
{suffix}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if variant === "horizontal" && showButtons}
|
||||
<button
|
||||
type="button"
|
||||
aria-label='{incrementLabel}'
|
||||
aria-controls='{inputId()}'
|
||||
disabled='{incrementDisabled()}'
|
||||
@click='incrementValue(event)'
|
||||
class='inline-flex shrink-0 items-center justify-center border-l border-[var(--wire-color-border)] bg-transparent text-[var(--wire-color-muted)] transition-[color,background-color] duration-[var(--wire-motion-fast)] hover:bg-[var(--wire-color-surface-2)] hover:text-[var(--input-number-accent)] focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--input-number-accent)] disabled:cursor-not-allowed disabled:opacity-40 {size === "xs" ? "w-8" : size === "sm" ? "w-9" : size === "lg" ? "w-12" : size === "xl" ? "w-14" : "w-10"}'
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class='icon-[lucide--plus] size-4'
|
||||
></span>
|
||||
</button>
|
||||
{:else}
|
||||
{#if showButtons}
|
||||
<div
|
||||
role="group"
|
||||
aria-label='{controlsLabel}'
|
||||
class='flex shrink-0 border-l border-[var(--wire-color-border)] {variant === "vertical" ? "flex-col" : "flex-row"}'
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label='{decrementLabel}'
|
||||
aria-controls='{inputId()}'
|
||||
disabled='{decrementDisabled()}'
|
||||
@click='decrementValue(event)'
|
||||
class='inline-flex items-center justify-center bg-transparent text-[var(--wire-color-muted)] transition-[color,background-color] duration-[var(--wire-motion-fast)] hover:bg-[var(--wire-color-surface-2)] hover:text-[var(--input-number-accent)] focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--input-number-accent)] disabled:cursor-not-allowed disabled:opacity-40 {variant === "vertical" ? "flex-1 border-b border-[var(--wire-color-border)]" : "border-r border-[var(--wire-color-border)]"} {size === "xs" ? "w-8" : size === "sm" ? "w-9" : size === "lg" ? "w-12" : size === "xl" ? "w-14" : "w-10"}'
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class='icon-[lucide--minus] size-4'
|
||||
></span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
aria-label='{incrementLabel}'
|
||||
aria-controls='{inputId()}'
|
||||
disabled='{incrementDisabled()}'
|
||||
@click='incrementValue(event)'
|
||||
class='inline-flex items-center justify-center bg-transparent text-[var(--wire-color-muted)] transition-[color,background-color] duration-[var(--wire-motion-fast)] hover:bg-[var(--wire-color-surface-2)] hover:text-[var(--input-number-accent)] focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--input-number-accent)] disabled:cursor-not-allowed disabled:opacity-40 {variant === "vertical" ? "flex-1" : ""} {size === "xs" ? "w-8" : size === "sm" ? "w-9" : size === "lg" ? "w-12" : size === "xl" ? "w-14" : "w-10"}'
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class='icon-[lucide--plus] size-4'
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if showValidationMessage && isInvalid() && validationMessage() !== ""}
|
||||
<p
|
||||
id='{messageId()}'
|
||||
role="alert"
|
||||
aria-live="polite"
|
||||
class='m-0 flex items-center gap-1.5 text-xs leading-5 text-[var(--wire-color-danger)]'
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class='icon-[lucide--circle-alert] size-3.5 shrink-0'
|
||||
></span>
|
||||
<span>{validationMessage()}</span>
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if (!showValidationMessage || !isInvalid() || validationMessage() === "") && helpText !== ""}
|
||||
<p
|
||||
id='{messageId()}'
|
||||
class='m-0 text-xs leading-5 text-[var(--wire-color-muted)]'
|
||||
>
|
||||
{helpText}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
component Kbd {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "⌘ K"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
label: string = "⌘ K"
|
||||
class: string = ""
|
||||
}
|
||||
view { <kbd class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--kbd {class}"><slot>{label}</slot></kbd> }
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
component LayoutSplitter {
|
||||
outputs {
|
||||
resizeStart(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
|
||||
resize(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
|
||||
resizeEnd(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
columns = 2
|
||||
gap = "md"
|
||||
maxWidth = "xl"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
columns: number = 2
|
||||
gap: string = "md"
|
||||
maxWidth: string = "xl"
|
||||
class: string = ""
|
||||
}
|
||||
view {
|
||||
<div class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--layout-splitter wire-next--gap-{gap} wire-next--columns-{columns} wire-next--max-{maxWidth} {class}"><slot /></div>
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
component LegendIndicator {
|
||||
outputs {
|
||||
toggle(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Legend Indicator"
|
||||
description = ""
|
||||
items = []
|
||||
variant = "default"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
title: string = "Legend Indicator"
|
||||
description: string = ""
|
||||
items: unknown[] = []
|
||||
variant: string = "default"
|
||||
class: string = ""
|
||||
}
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--legend-indicator wire-next--variant-{variant} {class}">
|
||||
|
||||
@@ -1,15 +1,47 @@
|
||||
component Link {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "Link"
|
||||
href = "#"
|
||||
target = ""
|
||||
rel = ""
|
||||
external = false
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
label: string = "Link"
|
||||
href: string = "#"
|
||||
target: string = ""
|
||||
rel: string = ""
|
||||
external: boolean = false
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
view {
|
||||
<a {...attrs} href="{href}" target="{target}" rel="{rel}" class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--link {class}"><slot>{label}</slot>{#if external}<span aria-hidden="true">↗</span>{/if}</a>
|
||||
<a
|
||||
data-ui-component="Link"
|
||||
href='{href}'
|
||||
target='{target}'
|
||||
rel='{external ? (rel || "noopener noreferrer") : rel}'
|
||||
class='inline-flex min-w-0 items-center gap-1.5 rounded-md font-semibold underline-offset-4 outline-none transition-colors focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--wire-color-background)] {class}'
|
||||
class:text-xs='size === "xs"'
|
||||
class:text-sm='size === "sm" || size === "default"'
|
||||
class:text-base='size === "md"'
|
||||
class:text-lg='size === "lg"'
|
||||
class:text-[var(--wire-color-primary)]='color === "primary"'
|
||||
class:hover:text-[var(--wire-color-primary-hover)]='color === "primary"'
|
||||
class:text-[var(--wire-color-secondary)]='color === "secondary"'
|
||||
class:hover:text-[var(--wire-color-secondary-hover)]='color === "secondary"'
|
||||
class:text-[var(--wire-color-success)]='color === "success"'
|
||||
class:text-[var(--wire-color-warning-text)]='color === "warning"'
|
||||
class:text-[var(--wire-color-danger)]='color === "danger"'
|
||||
class:text-[var(--wire-color-info)]='color === "info"'
|
||||
class:text-[var(--wire-color-text)]='color === "neutral"'
|
||||
class:hover:underline='external === false'
|
||||
>
|
||||
<span class="truncate">{label}</span>
|
||||
|
||||
{#if external}
|
||||
<span
|
||||
class="icon-[lucide--external-link] size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
{/if}
|
||||
|
||||
<slot></slot>
|
||||
</a>
|
||||
}
|
||||
}
|
||||
|
||||
+100
-12
@@ -1,19 +1,107 @@
|
||||
component List {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "List"
|
||||
description = ""
|
||||
items = []
|
||||
variant = "default"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
title: string = "List"
|
||||
description: string = ""
|
||||
items: unknown[] = []
|
||||
variant: string = "default"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--list wire-next--variant-{variant} {class}">
|
||||
{#if title}<strong>{title}</strong>{/if}
|
||||
{#if description}<p>{description}</p>{/if}
|
||||
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
|
||||
<slot />
|
||||
<section
|
||||
data-ui-component="List"
|
||||
aria-label='{title}'
|
||||
class='w-full {class}'
|
||||
>
|
||||
{#if title || description}
|
||||
<header class="mb-4">
|
||||
{#if title}
|
||||
<h3 class="text-lg font-bold text-[var(--wire-color-text)]">{title}</h3>
|
||||
{/if}
|
||||
{#if description}
|
||||
<p class="mt-1 text-sm leading-6 text-[var(--wire-color-text-muted)]">{description}</p>
|
||||
{/if}
|
||||
</header>
|
||||
{/if}
|
||||
|
||||
<ul
|
||||
class="overflow-hidden"
|
||||
class:divide-y='variant === "default" || variant === "divided"'
|
||||
class:divide-[var(--wire-color-border)]='variant === "default" || variant === "divided"'
|
||||
class:rounded-2xl='variant === "card"'
|
||||
class:border='variant === "card"'
|
||||
class:border-[var(--wire-color-border)]='variant === "card"'
|
||||
class:bg-[var(--wire-color-surface-raised)]='variant === "card"'
|
||||
class:space-y-3='variant === "separated"'
|
||||
>
|
||||
{#each items as item, index}
|
||||
<li
|
||||
class="group min-w-0"
|
||||
class:rounded-xl='variant === "separated"'
|
||||
class:border='variant === "separated"'
|
||||
class:border-[var(--wire-color-border)]='variant === "separated"'
|
||||
class:bg-[var(--wire-color-surface-raised)]='variant === "separated"'
|
||||
>
|
||||
{#if item.href}
|
||||
<a
|
||||
href='{item.href}'
|
||||
class="flex min-w-0 items-start gap-3 px-4 py-3 outline-none transition hover:bg-[var(--wire-color-surface-soft)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--wire-color-focus)]"
|
||||
class:px-3='size === "sm"'
|
||||
class:py-2.5='size === "sm"'
|
||||
class:px-5='size === "lg"'
|
||||
class:py-4='size === "lg"'
|
||||
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: item, index: index } }))'
|
||||
>
|
||||
{#if item.icon}
|
||||
<span class="flex size-10 shrink-0 items-center justify-center rounded-xl bg-[var(--wire-color-primary-soft)] text-[var(--wire-color-primary)]">
|
||||
<span class='{item.icon + " size-5"}' aria-hidden="true"></span>
|
||||
</span>
|
||||
{/if}
|
||||
{#if item.imageSrc}
|
||||
<img src='{item.imageSrc}' alt='{item.imageAlt || ""}' class="size-12 shrink-0 rounded-xl object-cover" loading="lazy" />
|
||||
{/if}
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<p class="truncate font-semibold text-[var(--wire-color-text)]">{item.title || item.label}</p>
|
||||
{#if item.meta}
|
||||
<span class="shrink-0 text-xs text-[var(--wire-color-text-muted)]">{item.meta}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if item.description}
|
||||
<p class="mt-1 line-clamp-2 text-sm leading-5 text-[var(--wire-color-text-muted)]">{item.description}</p>
|
||||
{/if}
|
||||
{#if item.badge}
|
||||
<span class="mt-2 inline-flex rounded-full bg-[var(--wire-color-primary-soft)] px-2.5 py-1 text-xs font-semibold text-[var(--wire-color-primary)]">{item.badge}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="icon-[lucide--chevron-right] mt-1 size-4 shrink-0 text-[var(--wire-color-text-muted)] transition group-hover:translate-x-0.5 group-hover:text-[var(--wire-color-primary)]" aria-hidden="true"></span>
|
||||
</a>
|
||||
{:else}
|
||||
<div class="flex min-w-0 items-start gap-3 px-4 py-3">
|
||||
{#if item.icon}
|
||||
<span class="flex size-10 shrink-0 items-center justify-center rounded-xl bg-[var(--wire-color-primary-soft)] text-[var(--wire-color-primary)]">
|
||||
<span class='{item.icon + " size-5"}' aria-hidden="true"></span>
|
||||
</span>
|
||||
{/if}
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="font-semibold text-[var(--wire-color-text)]">{item.title || item.label}</p>
|
||||
{#if item.description}
|
||||
<p class="mt-1 text-sm leading-5 text-[var(--wire-color-text-muted)]">{item.description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</li>
|
||||
{:empty}
|
||||
<li class="rounded-xl border border-dashed border-[var(--wire-color-border)] p-6 text-center text-sm text-[var(--wire-color-text-muted)]">
|
||||
No items available.
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<slot></slot>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
component ListGroup {
|
||||
outputs {
|
||||
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "List Group"
|
||||
description = ""
|
||||
items = []
|
||||
variant = "default"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
title: string = "List Group"
|
||||
description: string = ""
|
||||
items: unknown[] = []
|
||||
variant: string = "default"
|
||||
class: string = ""
|
||||
}
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--list-group wire-next--variant-{variant} {class}">
|
||||
|
||||
@@ -1,19 +1,85 @@
|
||||
component Map {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Map"
|
||||
description = ""
|
||||
items = []
|
||||
variant = "default"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
title: string = "Map"
|
||||
description: string = ""
|
||||
items: unknown[] = []
|
||||
variant: string = "default"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--map wire-next--variant-{variant} {class}">
|
||||
{#if title}<strong>{title}</strong>{/if}
|
||||
{#if description}<p>{description}</p>{/if}
|
||||
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
|
||||
<slot />
|
||||
<section
|
||||
data-ui-component="Map"
|
||||
aria-label='{title}'
|
||||
class='overflow-hidden rounded-2xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-raised)] shadow-sm {class}'
|
||||
>
|
||||
{#if title || description}
|
||||
<header class="flex items-start justify-between gap-4 border-b border-[var(--wire-color-border)] p-5">
|
||||
<div>
|
||||
{#if title}
|
||||
<h3 class="text-lg font-bold text-[var(--wire-color-text)]">{title}</h3>
|
||||
{/if}
|
||||
{#if description}
|
||||
<p class="mt-1 text-sm leading-6 text-[var(--wire-color-text-muted)]">{description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="flex size-10 shrink-0 items-center justify-center rounded-xl bg-[var(--wire-color-primary-soft)] text-[var(--wire-color-primary)]">
|
||||
<span class="icon-[lucide--map] size-5" aria-hidden="true"></span>
|
||||
</span>
|
||||
</header>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="relative isolate overflow-hidden bg-[var(--wire-color-surface-soft)]"
|
||||
class:h-64='size === "sm"'
|
||||
class:h-80='size === "default" || size === "md"'
|
||||
class:h-[28rem]='size === "lg"'
|
||||
>
|
||||
<div
|
||||
class="pointer-events-none absolute inset-0 opacity-50"
|
||||
style="background-image: linear-gradient(var(--wire-color-border) 1px, transparent 1px), linear-gradient(90deg, var(--wire-color-border) 1px, transparent 1px); background-size: 32px 32px;"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
|
||||
<div class="absolute inset-0 flex items-center justify-center p-6">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
{#if items.length > 0}
|
||||
{#each items as item, index}
|
||||
<button
|
||||
type="button"
|
||||
aria-label='{item.label || item.title || "Map marker"}'
|
||||
class="absolute inline-flex size-10 items-center justify-center rounded-full border-4 border-[var(--wire-color-surface-raised)] bg-[var(--wire-color-primary)] text-[var(--wire-color-on-primary)] shadow-lg transition hover:scale-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)]"
|
||||
style='left: {item.x || (20 + index * 12)}%; top: {item.y || (30 + (index % 3) * 18)}%;'
|
||||
@click='event.currentTarget.dispatchEvent(new CustomEvent("markerClick", { bubbles: true, detail: item })); event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: item }))'
|
||||
>
|
||||
<span class='{item.icon || "icon-[lucide--map-pin]"}' aria-hidden="true"></span>
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<div class="absolute bottom-4 right-4 flex flex-col gap-2">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Zoom in"
|
||||
class="inline-flex size-10 items-center justify-center rounded-xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-raised)] text-[var(--wire-color-text)] shadow-sm transition hover:bg-[var(--wire-color-surface-soft)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)]"
|
||||
@click='event.currentTarget.dispatchEvent(new CustomEvent("zoom", { bubbles: true, detail: { direction: "in" } }))'
|
||||
>
|
||||
<span class="icon-[lucide--plus] size-4" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Zoom out"
|
||||
class="inline-flex size-10 items-center justify-center rounded-xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-raised)] text-[var(--wire-color-text)] shadow-sm transition hover:bg-[var(--wire-color-surface-soft)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)]"
|
||||
@click='event.currentTarget.dispatchEvent(new CustomEvent("zoom", { bubbles: true, detail: { direction: "out" } }))'
|
||||
>
|
||||
<span class="icon-[lucide--minus] size-4" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,111 @@
|
||||
component Marquee {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Marquee"
|
||||
description = ""
|
||||
items = []
|
||||
variant = "default"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
title: string = "Marquee"
|
||||
description: string = ""
|
||||
items: unknown[] = []
|
||||
variant: string = "default"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
state paused: boolean = false
|
||||
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--marquee wire-next--variant-{variant} {class}">
|
||||
{#if title}<strong>{title}</strong>{/if}
|
||||
{#if description}<p>{description}</p>{/if}
|
||||
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
|
||||
<slot />
|
||||
<section
|
||||
data-ui-component="Marquee"
|
||||
aria-label='{title}'
|
||||
class='overflow-hidden border-y border-[var(--wire-color-border)] bg-[var(--wire-color-surface-raised)] {class}'
|
||||
>
|
||||
<div class="flex items-stretch">
|
||||
{#if title}
|
||||
<div class="relative z-10 flex shrink-0 items-center gap-2 border-r border-[var(--wire-color-border)] bg-[var(--wire-color-primary)] px-4 py-3 text-[var(--wire-color-on-primary)] sm:px-5">
|
||||
<span class="icon-[lucide--megaphone] size-4" aria-hidden="true"></span>
|
||||
<span class="text-sm font-bold">{title}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="group relative min-w-0 flex-1 overflow-hidden"
|
||||
@mouseenter='paused = true; event.currentTarget.dispatchEvent(new CustomEvent("pause", { bubbles: true }))'
|
||||
@mouseleave='paused = false; event.currentTarget.dispatchEvent(new CustomEvent("resume", { bubbles: true }))'
|
||||
@focusin='paused = true'
|
||||
@focusout='paused = false'
|
||||
>
|
||||
<div
|
||||
class="wire-marquee-track flex w-max min-w-full items-center"
|
||||
class:wire-marquee-paused='paused'
|
||||
>
|
||||
{#each items as item}
|
||||
<a
|
||||
href='{item.href || "#"}'
|
||||
class="flex shrink-0 items-center gap-3 px-5 py-3 text-sm font-medium text-[var(--wire-color-text)] transition hover:bg-[var(--wire-color-primary-soft)] hover:text-[var(--wire-color-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--wire-color-focus)]"
|
||||
>
|
||||
<span
|
||||
class="size-2 shrink-0 rounded-full bg-[var(--wire-color-primary)]"
|
||||
class:bg-[var(--wire-color-danger)]='item.color === "danger"'
|
||||
class:bg-[var(--wire-color-warning)]='item.color === "warning"'
|
||||
class:bg-[var(--wire-color-success)]='item.color === "success"'
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span>{item.label || item.title}</span>
|
||||
{#if item.meta}
|
||||
<span class="text-xs text-[var(--wire-color-text-muted)]">{item.meta}</span>
|
||||
{/if}
|
||||
</a>
|
||||
{:empty}
|
||||
<p class="px-5 py-3 text-sm text-[var(--wire-color-text-muted)]">{description || "No announcements available."}</p>
|
||||
{/each}
|
||||
|
||||
{#if items.length > 0}
|
||||
{#each items as item}
|
||||
<a
|
||||
href='{item.href || "#"}'
|
||||
aria-hidden="true"
|
||||
tabindex="-1"
|
||||
class="flex shrink-0 items-center gap-3 px-5 py-3 text-sm font-medium text-[var(--wire-color-text)]"
|
||||
>
|
||||
<span class="size-2 shrink-0 rounded-full bg-[var(--wire-color-primary)]" aria-hidden="true"></span>
|
||||
<span>{item.label || item.title}</span>
|
||||
</a>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
aria-label='{paused ? "Resume announcements" : "Pause announcements"}'
|
||||
class="flex shrink-0 items-center justify-center border-l border-[var(--wire-color-border)] px-4 text-[var(--wire-color-text-muted)] transition hover:bg-[var(--wire-color-surface-soft)] hover:text-[var(--wire-color-text)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--wire-color-focus)]"
|
||||
@click='paused = !paused; event.currentTarget.dispatchEvent(new CustomEvent(paused ? "pause" : "resume", { bubbles: true }))'
|
||||
>
|
||||
<span class="icon-[lucide--pause] size-4" data-show='!paused' aria-hidden="true"></span>
|
||||
<span class="icon-[lucide--play] size-4" data-show='paused' aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<slot></slot>
|
||||
</section>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-marquee-track {
|
||||
animation: wire-marquee 32s linear infinite;
|
||||
}
|
||||
|
||||
.wire-marquee-paused {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
@keyframes wire-marquee {
|
||||
from { transform: translateX(0); }
|
||||
to { transform: translateX(-50%); }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wire-marquee-track {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
component MegaMenu {
|
||||
outputs {
|
||||
open(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
close(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "Mega Menu"
|
||||
items = []
|
||||
active = ""
|
||||
orientation = "horizontal"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
label: string = "Mega Menu"
|
||||
items: unknown[] = []
|
||||
active: string = ""
|
||||
orientation: string = "horizontal"
|
||||
class: string = ""
|
||||
}
|
||||
view {
|
||||
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--mega-menu wire-next--{orientation} {class}" aria-label="{label}">
|
||||
|
||||
@@ -1,19 +1,608 @@
|
||||
component Modal {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Modal"
|
||||
description = ""
|
||||
open = false
|
||||
placement = "bottom"
|
||||
closeLabel = "Close"
|
||||
class = ""
|
||||
outputs {
|
||||
open(payload: { sourceEvent: Event })
|
||||
close(payload: { reason: string; sourceEvent: Event })
|
||||
cancel(payload: { sourceEvent: Event })
|
||||
confirm(payload: { sourceEvent: Event })
|
||||
}
|
||||
|
||||
props {
|
||||
|
||||
open: boolean = false
|
||||
defaultOpen: boolean = false
|
||||
title: string = "Modal"
|
||||
description: string = ""
|
||||
icon: string = ""
|
||||
label: string = "Modal dialog"
|
||||
size: string = "md"
|
||||
placement: string = "center"
|
||||
color: string = "primary"
|
||||
variant: string = "default"
|
||||
showClose: boolean = true
|
||||
closeLabel: string = "Close modal"
|
||||
closeOnBackdrop: boolean = true
|
||||
closeOnEscape: boolean = true
|
||||
closeOnCancel: boolean = true
|
||||
closeOnConfirm: boolean = false
|
||||
showFooter: boolean = true
|
||||
cancelLabel: string = "Cancel"
|
||||
cancelIcon: string = ""
|
||||
confirmLabel: string = "Confirm"
|
||||
confirmIcon: string = ""
|
||||
confirmDisabled: boolean = false
|
||||
confirmLoading: boolean = false
|
||||
destructive: boolean = false
|
||||
triggerLabel: string = ""
|
||||
triggerIcon: string = ""
|
||||
scrollable: boolean = true
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
state visible = defaultOpen
|
||||
|
||||
functions {
|
||||
shared function isOpen() {
|
||||
return open || visible
|
||||
}
|
||||
|
||||
client function showModal(sourceEvent) {
|
||||
visible = true
|
||||
output.open({ sourceEvent: sourceEvent })
|
||||
}
|
||||
|
||||
client function hideModal(reason, sourceEvent) {
|
||||
visible = false
|
||||
output.close({
|
||||
reason: reason,
|
||||
sourceEvent: sourceEvent
|
||||
})
|
||||
}
|
||||
|
||||
client function cancelModal(sourceEvent) {
|
||||
output.cancel({ sourceEvent: sourceEvent })
|
||||
if (closeOnCancel) {
|
||||
hideModal("cancel", sourceEvent)
|
||||
}
|
||||
}
|
||||
|
||||
client function confirmModal(sourceEvent) {
|
||||
if (confirmDisabled || confirmLoading) {
|
||||
return
|
||||
}
|
||||
output.confirm({ sourceEvent: sourceEvent })
|
||||
if (closeOnConfirm) {
|
||||
hideModal("confirm", sourceEvent)
|
||||
}
|
||||
}
|
||||
|
||||
client function handleKeydown(sourceEvent) {
|
||||
if (closeOnEscape && sourceEvent.key === "Escape") {
|
||||
sourceEvent.preventDefault()
|
||||
cancelModal(sourceEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<div data-show="{open}" class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--modal wire-next--placement-{placement} {class}" role="dialog" aria-modal="true" aria-label="{title}">
|
||||
<header><strong>{title}</strong><button type="button" aria-label="{closeLabel}">×</button></header>
|
||||
{#if description}<p>{description}</p>{/if}
|
||||
<slot />
|
||||
<div
|
||||
{...attrs}
|
||||
data-ui-component="Modal"
|
||||
data-open='{open || visible ? "true" : "false"}'
|
||||
data-size='{size}'
|
||||
data-placement='{placement}'
|
||||
data-color='{color}'
|
||||
data-variant='{variant}'
|
||||
data-scrollable='{scrollable ? "true" : "false"}'
|
||||
data-destructive='{destructive ? "true" : "false"}'
|
||||
class='wire-modal {class}'
|
||||
>
|
||||
{#if triggerLabel}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-modal__trigger"
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded='{open || visible ? "true" : "false"}'
|
||||
@click='showModal(event)'
|
||||
>
|
||||
{#if triggerIcon}
|
||||
<span
|
||||
class='{triggerIcon}'
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
{/if}
|
||||
<span>{triggerLabel}</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<span
|
||||
class="wire-modal__trigger-slot"
|
||||
@click='showModal(event)'
|
||||
>
|
||||
<slot
|
||||
name="trigger"
|
||||
>
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<div
|
||||
class="wire-modal__layer"
|
||||
data-show='{open || visible}'
|
||||
role="presentation"
|
||||
@keydown='handleKeydown(event)'
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="wire-modal__backdrop"
|
||||
aria-label='{closeLabel}'
|
||||
@click='if (closeOnBackdrop) { cancelModal(event) }'
|
||||
>
|
||||
</button>
|
||||
|
||||
<section
|
||||
class="wire-modal__panel"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label='{label || title}'
|
||||
tabindex="-1"
|
||||
>
|
||||
<header
|
||||
class="wire-modal__header"
|
||||
>
|
||||
<div
|
||||
class="wire-modal__heading"
|
||||
>
|
||||
{#if icon}
|
||||
<span
|
||||
class='wire-modal__icon {icon}'
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="wire-modal__heading-copy"
|
||||
>
|
||||
<slot
|
||||
name="header"
|
||||
>
|
||||
</slot>
|
||||
{#if title}
|
||||
<h2>{title}</h2>
|
||||
{/if}
|
||||
{#if description}
|
||||
<p>{description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showClose}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-modal__close"
|
||||
aria-label='{closeLabel}'
|
||||
@click='hideModal("close-button", event)'
|
||||
>
|
||||
<span
|
||||
class="icon-[lucide--x]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
</button>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
<div
|
||||
class="wire-modal__body"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
{#if showFooter}
|
||||
<footer
|
||||
class="wire-modal__footer"
|
||||
>
|
||||
<div
|
||||
class="wire-modal__custom-footer"
|
||||
>
|
||||
<slot
|
||||
name="footer"
|
||||
>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="wire-modal__actions"
|
||||
>
|
||||
{#if cancelLabel}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-modal__button wire-modal__button--secondary"
|
||||
@click='cancelModal(event)'
|
||||
>
|
||||
{#if cancelIcon}
|
||||
<span
|
||||
class='{cancelIcon}'
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
{/if}
|
||||
<span>{cancelLabel}</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if confirmLabel}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-modal__button wire-modal__button--primary"
|
||||
disabled='{confirmDisabled || confirmLoading}'
|
||||
@click='confirmModal(event)'
|
||||
>
|
||||
{#if confirmLoading}
|
||||
<span
|
||||
class="wire-modal__spinner"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
{:else if confirmIcon}
|
||||
<span
|
||||
class='{confirmIcon}'
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
{/if}
|
||||
<span>{confirmLabel}</span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</footer>
|
||||
{/if}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-modal {
|
||||
--modal-accent: var(--wire-color-primary);
|
||||
--modal-soft: var(--wire-color-primary-soft);
|
||||
--modal-contrast: var(--wire-color-primary-contrast);
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.wire-modal[data-color="secondary"] {
|
||||
--modal-accent: var(--wire-color-secondary);
|
||||
--modal-soft: var(--wire-color-secondary-soft);
|
||||
--modal-contrast: var(--wire-color-secondary-contrast);
|
||||
}
|
||||
|
||||
.wire-modal[data-color="info"] {
|
||||
--modal-accent: var(--wire-color-info);
|
||||
--modal-soft: var(--wire-color-info-soft);
|
||||
--modal-contrast: var(--wire-color-info-contrast);
|
||||
}
|
||||
|
||||
.wire-modal[data-color="success"] {
|
||||
--modal-accent: var(--wire-color-success);
|
||||
--modal-soft: var(--wire-color-success-soft);
|
||||
--modal-contrast: var(--wire-color-success-contrast);
|
||||
}
|
||||
|
||||
.wire-modal[data-color="warning"] {
|
||||
--modal-accent: var(--wire-color-warning);
|
||||
--modal-soft: var(--wire-color-warning-soft);
|
||||
--modal-contrast: var(--wire-color-warning-text);
|
||||
}
|
||||
|
||||
.wire-modal[data-color="danger"],
|
||||
.wire-modal[data-destructive="true"] {
|
||||
--modal-accent: var(--wire-color-danger);
|
||||
--modal-soft: var(--wire-color-danger-soft);
|
||||
--modal-contrast: var(--wire-color-on-danger);
|
||||
}
|
||||
|
||||
.wire-modal__trigger,
|
||||
.wire-modal__trigger-slot {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
.wire-modal__trigger {
|
||||
appearance: none;
|
||||
min-height: 2.55rem;
|
||||
padding: 0.65rem 1rem;
|
||||
color: var(--modal-contrast);
|
||||
background: var(--modal-accent);
|
||||
border: 0;
|
||||
border-radius: 0.8rem;
|
||||
font: inherit;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 650;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wire-modal__layer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1250;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.wire-modal[data-placement="top"] .wire-modal__layer {
|
||||
align-items: flex-start;
|
||||
padding-top: clamp(1rem, 8vh, 5rem);
|
||||
}
|
||||
|
||||
.wire-modal__backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
appearance: none;
|
||||
padding: 0;
|
||||
background: color-mix(in srgb, black 62%, transparent);
|
||||
border: 0;
|
||||
backdrop-filter: blur(9px);
|
||||
}
|
||||
|
||||
.wire-modal__panel {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: min(32rem, calc(100vw - 2rem));
|
||||
max-height: min(88vh, 52rem);
|
||||
color: var(--wire-color-text);
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 100% 0%,
|
||||
color-mix(in srgb, var(--modal-accent) 9%, transparent),
|
||||
transparent 34%
|
||||
),
|
||||
var(--wire-color-surface-raised);
|
||||
border: 1px solid color-mix(in srgb, var(--modal-accent) 18%, var(--wire-color-border));
|
||||
border-radius: 1.35rem;
|
||||
box-shadow:
|
||||
0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
|
||||
0 40px 110px color-mix(in srgb, black 38%, transparent);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wire-modal[data-size="sm"] .wire-modal__panel {
|
||||
width: min(25rem, calc(100vw - 2rem));
|
||||
}
|
||||
|
||||
.wire-modal[data-size="lg"] .wire-modal__panel {
|
||||
width: min(44rem, calc(100vw - 2rem));
|
||||
}
|
||||
|
||||
.wire-modal[data-size="xl"] .wire-modal__panel {
|
||||
width: min(64rem, calc(100vw - 2rem));
|
||||
}
|
||||
|
||||
.wire-modal[data-size="full"] .wire-modal__panel {
|
||||
width: calc(100vw - 2rem);
|
||||
height: calc(100vh - 2rem);
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.wire-modal[data-variant="soft"] .wire-modal__panel {
|
||||
background:
|
||||
linear-gradient(145deg, var(--modal-soft), transparent 68%),
|
||||
var(--wire-color-surface-raised);
|
||||
}
|
||||
|
||||
.wire-modal[data-variant="outline"] .wire-modal__panel {
|
||||
background: var(--wire-color-surface-raised);
|
||||
box-shadow: 0 24px 72px color-mix(in srgb, black 24%, transparent);
|
||||
}
|
||||
|
||||
.wire-modal[data-variant="solid"] .wire-modal__panel {
|
||||
color: var(--modal-contrast);
|
||||
background: var(--modal-accent);
|
||||
border-color: color-mix(in srgb, white 20%, transparent);
|
||||
}
|
||||
|
||||
.wire-modal__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1.4rem 1.4rem 1.15rem;
|
||||
border-bottom: 1px solid var(--wire-color-border);
|
||||
}
|
||||
|
||||
.wire-modal__heading {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.9rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-modal__icon {
|
||||
flex: 0 0 auto;
|
||||
width: 1.3rem;
|
||||
height: 1.3rem;
|
||||
margin-top: 0.15rem;
|
||||
color: var(--modal-accent);
|
||||
}
|
||||
|
||||
.wire-modal[data-variant="solid"] .wire-modal__icon {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.wire-modal__heading-copy {
|
||||
display: grid;
|
||||
gap: 0.3rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-modal__heading-copy h2,
|
||||
.wire-modal__heading-copy p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wire-modal__heading-copy h2 {
|
||||
font-size: 1.08rem;
|
||||
font-weight: 650;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.wire-modal__heading-copy p {
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.wire-modal[data-variant="solid"] .wire-modal__heading-copy p {
|
||||
color: color-mix(in srgb, currentColor 76%, transparent);
|
||||
}
|
||||
|
||||
.wire-modal__close {
|
||||
appearance: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
width: 2.35rem;
|
||||
height: 2.35rem;
|
||||
color: var(--wire-color-text-muted);
|
||||
background: var(--wire-color-surface-soft);
|
||||
border: 1px solid var(--wire-color-border);
|
||||
border-radius: 0.75rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wire-modal__close:hover,
|
||||
.wire-modal__close:focus-visible {
|
||||
color: var(--modal-accent);
|
||||
border-color: color-mix(in srgb, var(--modal-accent) 34%, var(--wire-color-border));
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.wire-modal__body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
padding: 1.4rem;
|
||||
}
|
||||
|
||||
.wire-modal[data-scrollable="true"] .wire-modal__body {
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.wire-modal__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1rem 1.4rem 1.4rem;
|
||||
border-top: 1px solid var(--wire-color-border);
|
||||
}
|
||||
|
||||
.wire-modal__custom-footer:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wire-modal__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.65rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.wire-modal__button {
|
||||
appearance: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
min-height: 2.55rem;
|
||||
padding: 0.65rem 1rem;
|
||||
border-radius: 0.78rem;
|
||||
font: inherit;
|
||||
font-size: 0.83rem;
|
||||
font-weight: 650;
|
||||
cursor: pointer;
|
||||
transition: transform 150ms ease, opacity 150ms ease, border-color 150ms ease;
|
||||
}
|
||||
|
||||
.wire-modal__button:active {
|
||||
transform: scale(0.985);
|
||||
}
|
||||
|
||||
.wire-modal__button--secondary {
|
||||
color: var(--wire-color-text);
|
||||
background: transparent;
|
||||
border: 1px solid var(--wire-color-border);
|
||||
}
|
||||
|
||||
.wire-modal__button--primary {
|
||||
color: var(--modal-contrast);
|
||||
background: var(--modal-accent);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.wire-modal__button:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.wire-modal__spinner {
|
||||
width: 0.95rem;
|
||||
height: 0.95rem;
|
||||
border: 2px solid color-mix(in srgb, currentColor 35%, transparent);
|
||||
border-top-color: currentColor;
|
||||
border-radius: 50%;
|
||||
animation: wire-modal-spin 750ms linear infinite;
|
||||
}
|
||||
|
||||
@keyframes wire-modal-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.wire-modal__layer {
|
||||
align-items: flex-end;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.wire-modal__panel,
|
||||
.wire-modal[data-size="sm"] .wire-modal__panel,
|
||||
.wire-modal[data-size="lg"] .wire-modal__panel,
|
||||
.wire-modal[data-size="xl"] .wire-modal__panel {
|
||||
width: 100%;
|
||||
max-height: 92vh;
|
||||
border-radius: 1.25rem 1.25rem 0 0;
|
||||
}
|
||||
|
||||
.wire-modal__footer {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.wire-modal__actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wire-modal__button {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wire-modal__button,
|
||||
.wire-modal__spinner {
|
||||
animation: none;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
component Nav {
|
||||
outputs {
|
||||
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "Nav"
|
||||
items = []
|
||||
active = ""
|
||||
orientation = "horizontal"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
label: string = "Nav"
|
||||
items: unknown[] = []
|
||||
active: string = ""
|
||||
orientation: string = "horizontal"
|
||||
class: string = ""
|
||||
}
|
||||
view {
|
||||
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--nav wire-next--{orientation} {class}" aria-label="{label}">
|
||||
|
||||
@@ -1,17 +1,143 @@
|
||||
component Navbar {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "Navbar"
|
||||
items = []
|
||||
active = ""
|
||||
orientation = "horizontal"
|
||||
class = ""
|
||||
outputs {
|
||||
toggle(payload: { open: boolean })
|
||||
open(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
close(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
select(payload: { item: string | number | boolean | null | object; value: string | number | boolean; level: string })
|
||||
action(payload: { item: string | number | boolean | null | object; value: string | number | boolean })
|
||||
}
|
||||
|
||||
props {
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
label: string = "Primary navigation"
|
||||
topbarLabel: string = "Utility navigation"
|
||||
brand: Record<string, unknown> = {}
|
||||
items: unknown[] = []
|
||||
actions: unknown[] = []
|
||||
active: string = ""
|
||||
sticky: boolean = false
|
||||
openOnHover: boolean = false
|
||||
maxWidth: string = "full"
|
||||
mobileLabel: string = "Toggle navigation"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
state mobileOpen: boolean = false
|
||||
|
||||
functions {
|
||||
client function toggleNavigation() {
|
||||
mobileOpen = !mobileOpen
|
||||
output.toggle({ open: mobileOpen })
|
||||
output[mobileOpen ? "open" : "close"]({ source: "mobile" })
|
||||
}
|
||||
|
||||
client function selectItem(item, level) {
|
||||
mobileOpen = false
|
||||
output.select({ item: item, value: item.value || "", level: level })
|
||||
}
|
||||
|
||||
client function selectAction(item) {
|
||||
mobileOpen = false
|
||||
output.action({ item: item, value: item.value || "" })
|
||||
}
|
||||
|
||||
shared function isItemActive(item) {
|
||||
if (!item) return false
|
||||
if (item.value && item.value === active) return true
|
||||
if (!item.children || !item.children.length) return false
|
||||
return item.children.some(function (child) {
|
||||
return isItemActive(child)
|
||||
})
|
||||
}
|
||||
|
||||
client function toggleDropdown(event, item) {
|
||||
output[event.currentTarget.open ? "open" : "close"]({
|
||||
source: "menu",
|
||||
item: item
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--navbar wire-next--{orientation} {class}" aria-label="{label}">
|
||||
{#each items as item}<a href="{item.href}" aria-current="{item.value === active ? 'page' : ''}">{item.label}</a>{/each}
|
||||
<slot />
|
||||
</nav>
|
||||
<header class="wire-navbar wire-navbar--width-{maxWidth} wire-next--color-{color} wire-next--size-{size} {sticky ? 'wire-navbar--sticky' : ''} {class}" data-wrn-navbar data-open-on-hover="{openOnHover}">
|
||||
<div class="wire-navbar__topbar" aria-label="{topbarLabel}">
|
||||
<slot name="topbar" />
|
||||
</div>
|
||||
|
||||
<div class="wire-navbar__main">
|
||||
<a class="wire-navbar__brand" href="{brand.href || '/'}" aria-label="{brand.ariaLabel || brand.label || label}">
|
||||
{#if brand.logo}
|
||||
<img class="wire-navbar__brand-logo" src="{brand.logo}" alt="{brand.logoAlt || brand.label || ''}" width="{brand.logoWidth || ''}" height="{brand.logoHeight || ''}" />
|
||||
{/if}
|
||||
{#if brand.icon}
|
||||
<span class="wire-navbar__brand-icon {brand.icon}" aria-hidden="true"></span>
|
||||
{/if}
|
||||
{#if brand.label || brand.description}
|
||||
<span class="wire-navbar__brand-copy">
|
||||
{#if brand.label}<strong>{brand.label}</strong>{/if}
|
||||
{#if brand.description}<small>{brand.description}</small>{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</a>
|
||||
|
||||
<button type="button" class="wire-navbar__toggle" aria-label="{mobileLabel}" aria-expanded="{mobileOpen}" @click="toggleNavigation()">
|
||||
<span aria-hidden="true"></span><span aria-hidden="true"></span><span aria-hidden="true"></span>
|
||||
</button>
|
||||
|
||||
<div class="wire-navbar__collapse {mobileOpen ? 'is-open' : ''}">
|
||||
<nav class="wire-navbar__menus" aria-label="{label}">
|
||||
{#each items as item}
|
||||
{#if item.children && item.children.length}
|
||||
<details class="wire-navbar__dropdown wire-navbar__dropdown--{item.type || 'dropdown'}" name="wire-navbar-menu" @toggle="toggleDropdown(event, item)">
|
||||
<summary aria-current="{isItemActive(item) ? 'page' : ''}">
|
||||
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
|
||||
<span>{item.label}</span>
|
||||
<span class="wire-navbar__chevron" aria-hidden="true"></span>
|
||||
</summary>
|
||||
<div class="wire-navbar__panel wire-navbar__panel--columns-{item.columns || 1}">
|
||||
{#if item.description}<p class="wire-navbar__panel-intro">{item.description}</p>{/if}
|
||||
{#each item.children as child}
|
||||
<div class="wire-navbar__group">
|
||||
{#if child.children && child.children.length}
|
||||
{#if child.label}<strong class="wire-navbar__group-title">{child.label}</strong>{/if}
|
||||
{#if child.description}<small>{child.description}</small>{/if}
|
||||
{#each child.children as nested}
|
||||
<a href="{nested.href || '#'}" target="{nested.target || ''}" rel="{nested.rel || ''}" aria-current="{nested.value === active ? 'page' : ''}" @click="selectItem(nested, 3)">
|
||||
{#if nested.icon}<span class="{nested.icon}" aria-hidden="true"></span>{/if}
|
||||
<span><strong>{nested.label}</strong>{#if nested.description}<small>{nested.description}</small>{/if}</span>
|
||||
</a>
|
||||
{/each}
|
||||
{:else}
|
||||
<a href="{child.href || '#'}" target="{child.target || ''}" rel="{child.rel || ''}" aria-current="{child.value === active ? 'page' : ''}" @click="selectItem(child, 2)">
|
||||
{#if child.icon}<span class="{child.icon}" aria-hidden="true"></span>{/if}
|
||||
<span><strong>{child.label}</strong>{#if child.description}<small>{child.description}</small>{/if}</span>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</details>
|
||||
{:else}
|
||||
<a class="wire-navbar__menu-link" href="{item.href || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" aria-current="{item.value === active ? 'page' : ''}" @click="selectItem(item, 1)">
|
||||
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
|
||||
<span>{item.label}</span>
|
||||
</a>
|
||||
{/if}
|
||||
{/each}
|
||||
</nav>
|
||||
|
||||
<div class="wire-navbar__actions">
|
||||
{#each actions as item}
|
||||
<a class="wire-navbar__action wire-navbar__action--{item.variant || 'link'}" href="{item.href || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" @click="selectAction(item)">
|
||||
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
|
||||
<span>{item.label}</span>
|
||||
</a>
|
||||
{/each}
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
component Pagination {
|
||||
outputs {
|
||||
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
previous(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
next(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "Pagination"
|
||||
items = []
|
||||
active = ""
|
||||
orientation = "horizontal"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
label: string = "Pagination"
|
||||
items: unknown[] = []
|
||||
active: string = ""
|
||||
orientation: string = "horizontal"
|
||||
class: string = ""
|
||||
}
|
||||
view {
|
||||
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--pagination wire-next--{orientation} {class}" aria-label="{label}">
|
||||
|
||||
@@ -1,41 +1,44 @@
|
||||
component PinInput {
|
||||
outputs {
|
||||
input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
complete(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
paste(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
|
||||
clear(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
error(payload: { error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "Verification code"
|
||||
name = "pin"
|
||||
value = ""
|
||||
length = 4
|
||||
pattern = "[0-9]"
|
||||
type = "text"
|
||||
inputMode = "numeric"
|
||||
placeholder = "○"
|
||||
autocomplete = "one-time-code"
|
||||
masked = false
|
||||
disabled = false
|
||||
readonly = false
|
||||
required = false
|
||||
autoFocus = false
|
||||
autoSubmit = false
|
||||
allowPaste = true
|
||||
clearable = true
|
||||
clearLabel = "Clear code"
|
||||
separator = ""
|
||||
groupSize = 0
|
||||
helpText = ""
|
||||
invalid = false
|
||||
validationMessage = ""
|
||||
class = ""
|
||||
@event input = function
|
||||
@event change = function
|
||||
@event complete = function
|
||||
@event paste = function
|
||||
@event clear = function
|
||||
@event error = function
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
label: string = "Verification code"
|
||||
name: string = "pin"
|
||||
value: string = ""
|
||||
length: number = 4
|
||||
pattern: string = "[0-9]"
|
||||
type: string = "text"
|
||||
inputMode: string = "numeric"
|
||||
placeholder: string = "○"
|
||||
autocomplete: string = "one-time-code"
|
||||
masked: boolean = false
|
||||
disabled: boolean = false
|
||||
readonly: boolean = false
|
||||
required: boolean = false
|
||||
autoFocus: boolean = false
|
||||
autoSubmit: boolean = false
|
||||
allowPaste: boolean = true
|
||||
clearable: boolean = true
|
||||
clearLabel: string = "Clear code"
|
||||
separator: string = ""
|
||||
groupSize: number = 0
|
||||
helpText: string = ""
|
||||
invalid: boolean = false
|
||||
validationMessage: string = ""
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
functions {
|
||||
function inputIndexes() {
|
||||
shared function inputIndexes() {
|
||||
return [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].slice(
|
||||
0,
|
||||
Math.max(1, Math.min(12, Number(length)))
|
||||
|
||||
@@ -1,19 +1,536 @@
|
||||
component Popover {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Popover"
|
||||
description = ""
|
||||
open = false
|
||||
placement = "bottom"
|
||||
closeLabel = "Close"
|
||||
class = ""
|
||||
outputs {
|
||||
toggle(payload: { open: boolean; sourceEvent: Event; reason?: object })
|
||||
open(payload: { sourceEvent: Event })
|
||||
close(payload: { reason: string; sourceEvent: Event })
|
||||
action(payload: { href: string; sourceEvent: Event })
|
||||
}
|
||||
|
||||
props {
|
||||
|
||||
open: boolean = false
|
||||
defaultOpen: boolean = false
|
||||
triggerLabel: string = "Open popover"
|
||||
triggerIcon: string = ""
|
||||
title: string = ""
|
||||
description: string = ""
|
||||
icon: string = ""
|
||||
placement: string = "bottom-start"
|
||||
width: string = "md"
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
variant: string = "raised"
|
||||
showArrow: boolean = true
|
||||
showClose: boolean = false
|
||||
closeLabel: string = "Close popover"
|
||||
closeOnOutside: boolean = true
|
||||
closeOnEscape: boolean = true
|
||||
actionLabel: string = ""
|
||||
actionHref: string = ""
|
||||
actionIcon: string = ""
|
||||
closeOnAction: boolean = true
|
||||
disabled: boolean = false
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
state visible = defaultOpen
|
||||
|
||||
functions {
|
||||
shared function isOpen() {
|
||||
return open || visible
|
||||
}
|
||||
|
||||
client function showPopover(sourceEvent) {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
visible = true
|
||||
output.open({ sourceEvent: sourceEvent })
|
||||
output.toggle({ open: true, sourceEvent: sourceEvent })
|
||||
}
|
||||
|
||||
client function hidePopover(reason, sourceEvent) {
|
||||
visible = false
|
||||
output.close({ reason: reason, sourceEvent: sourceEvent })
|
||||
output.toggle({ open: false, reason: reason, sourceEvent: sourceEvent })
|
||||
}
|
||||
|
||||
client function togglePopover(sourceEvent) {
|
||||
if (isOpen()) {
|
||||
hidePopover("toggle", sourceEvent)
|
||||
} else {
|
||||
showPopover(sourceEvent)
|
||||
}
|
||||
}
|
||||
|
||||
client function activateAction(sourceEvent) {
|
||||
output.action({
|
||||
href: actionHref,
|
||||
sourceEvent: sourceEvent
|
||||
})
|
||||
if (closeOnAction) {
|
||||
hidePopover("action", sourceEvent)
|
||||
}
|
||||
}
|
||||
|
||||
client function handleKeydown(sourceEvent) {
|
||||
if (closeOnEscape && sourceEvent.key === "Escape") {
|
||||
sourceEvent.preventDefault()
|
||||
hidePopover("escape", sourceEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<div data-show="{open}" class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--popover wire-next--placement-{placement} {class}" role="region" aria-modal="false" aria-label="{title}">
|
||||
<header><strong>{title}</strong><button type="button" aria-label="{closeLabel}">×</button></header>
|
||||
{#if description}<p>{description}</p>{/if}
|
||||
<slot />
|
||||
<div
|
||||
{...attrs}
|
||||
data-ui-component="Popover"
|
||||
data-open='{open || visible ? "true" : "false"}'
|
||||
data-placement='{placement}'
|
||||
data-width='{width}'
|
||||
data-size='{size}'
|
||||
data-color='{color}'
|
||||
data-variant='{variant}'
|
||||
class='wire-popover {class}'
|
||||
@keydown='handleKeydown(event)'
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="wire-popover__trigger"
|
||||
disabled='{disabled}'
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded='{open || visible ? "true" : "false"}'
|
||||
@click='togglePopover(event)'
|
||||
>
|
||||
<slot name="trigger"></slot>
|
||||
{#if triggerIcon}
|
||||
<span class='{triggerIcon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
{#if triggerLabel}
|
||||
<span>{triggerLabel}</span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if closeOnOutside}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-popover__dismiss-layer"
|
||||
data-show='{open || visible}'
|
||||
aria-label='{closeLabel}'
|
||||
@click='hidePopover("outside", event)'
|
||||
></button>
|
||||
{/if}
|
||||
|
||||
<section
|
||||
class="wire-popover__panel"
|
||||
data-show='{open || visible}'
|
||||
role="dialog"
|
||||
aria-label='{title || triggerLabel}'
|
||||
>
|
||||
{#if showArrow}
|
||||
<span class="wire-popover__arrow" aria-hidden="true"></span>
|
||||
{/if}
|
||||
|
||||
{#if title || description || icon || showClose}
|
||||
<header class="wire-popover__header">
|
||||
<div class="wire-popover__heading">
|
||||
{#if icon}
|
||||
<span class='wire-popover__icon {icon}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
<div class="wire-popover__heading-copy">
|
||||
<slot name="header"></slot>
|
||||
{#if title}
|
||||
<h3>{title}</h3>
|
||||
{/if}
|
||||
{#if description}
|
||||
<p>{description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showClose}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-popover__close"
|
||||
aria-label='{closeLabel}'
|
||||
@click='hidePopover("close-button", event)'
|
||||
>
|
||||
<span class="icon-[lucide--x]" aria-hidden="true"></span>
|
||||
</button>
|
||||
{/if}
|
||||
</header>
|
||||
{/if}
|
||||
|
||||
<div class="wire-popover__body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
{#if actionLabel}
|
||||
<footer class="wire-popover__footer">
|
||||
{#if actionHref}
|
||||
<a
|
||||
href='{actionHref}'
|
||||
class="wire-popover__action"
|
||||
@click='activateAction(event)'
|
||||
>
|
||||
<span>{actionLabel}</span>
|
||||
{#if actionIcon}
|
||||
<span class='{actionIcon}' aria-hidden="true"></span>
|
||||
{:else}
|
||||
<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
|
||||
{/if}
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-popover__action"
|
||||
@click='activateAction(event)'
|
||||
>
|
||||
<span>{actionLabel}</span>
|
||||
{#if actionIcon}
|
||||
<span class='{actionIcon}' aria-hidden="true"></span>
|
||||
{:else}
|
||||
<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</footer>
|
||||
{/if}
|
||||
|
||||
<slot name="footer"></slot>
|
||||
</section>
|
||||
</div>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-popover {
|
||||
--popover-accent: var(--wire-color-primary);
|
||||
--popover-soft: var(--wire-color-primary-soft);
|
||||
--popover-contrast: var(--wire-color-primary-contrast);
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.wire-popover[data-color="secondary"] {
|
||||
--popover-accent: var(--wire-color-secondary);
|
||||
--popover-soft: var(--wire-color-secondary-soft);
|
||||
--popover-contrast: var(--wire-color-secondary-contrast);
|
||||
}
|
||||
|
||||
.wire-popover[data-color="info"] {
|
||||
--popover-accent: var(--wire-color-info);
|
||||
--popover-soft: var(--wire-color-info-soft);
|
||||
--popover-contrast: var(--wire-color-info-contrast);
|
||||
}
|
||||
|
||||
.wire-popover[data-color="success"] {
|
||||
--popover-accent: var(--wire-color-success);
|
||||
--popover-soft: var(--wire-color-success-soft);
|
||||
--popover-contrast: var(--wire-color-success-contrast);
|
||||
}
|
||||
|
||||
.wire-popover[data-color="warning"] {
|
||||
--popover-accent: var(--wire-color-warning);
|
||||
--popover-soft: var(--wire-color-warning-soft);
|
||||
--popover-contrast: var(--wire-color-warning-text);
|
||||
}
|
||||
|
||||
.wire-popover[data-color="danger"] {
|
||||
--popover-accent: var(--wire-color-danger);
|
||||
--popover-soft: var(--wire-color-danger-soft);
|
||||
--popover-contrast: var(--wire-color-on-danger);
|
||||
}
|
||||
|
||||
.wire-popover__trigger {
|
||||
appearance: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.55rem;
|
||||
min-height: 2.55rem;
|
||||
padding: 0.65rem 0.9rem;
|
||||
color: var(--wire-color-text);
|
||||
background: var(--wire-color-surface-raised);
|
||||
border: 1px solid var(--wire-color-border);
|
||||
border-radius: 0.78rem;
|
||||
font: inherit;
|
||||
font-size: 0.84rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: color 150ms ease, border-color 150ms ease, background-color 150ms ease;
|
||||
}
|
||||
|
||||
.wire-popover__trigger:hover,
|
||||
.wire-popover__trigger:focus-visible,
|
||||
.wire-popover[data-open="true"] .wire-popover__trigger {
|
||||
color: var(--popover-accent);
|
||||
background: var(--popover-soft);
|
||||
border-color: color-mix(in srgb, var(--popover-accent) 38%, var(--wire-color-border));
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.wire-popover__trigger:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.wire-popover[data-size="sm"] .wire-popover__trigger {
|
||||
min-height: 2.2rem;
|
||||
padding: 0.5rem 0.72rem;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.wire-popover[data-size="lg"] .wire-popover__trigger {
|
||||
min-height: 2.9rem;
|
||||
padding: 0.78rem 1.05rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.wire-popover__dismiss-layer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1120;
|
||||
appearance: none;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.wire-popover__panel {
|
||||
position: absolute;
|
||||
z-index: 1121;
|
||||
top: calc(100% + 0.72rem);
|
||||
left: 0;
|
||||
width: 18rem;
|
||||
max-width: calc(100vw - 1.5rem);
|
||||
color: var(--wire-color-text);
|
||||
background:
|
||||
linear-gradient(145deg, color-mix(in srgb, var(--popover-accent) 5%, transparent), transparent 62%),
|
||||
color-mix(in srgb, var(--wire-color-surface-raised) 97%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--popover-accent) 18%, var(--wire-color-border));
|
||||
border-radius: 1rem;
|
||||
box-shadow:
|
||||
0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
|
||||
0 24px 64px color-mix(in srgb, black 22%, transparent);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.wire-popover[data-width="sm"] .wire-popover__panel {
|
||||
width: 14rem;
|
||||
}
|
||||
|
||||
.wire-popover[data-width="lg"] .wire-popover__panel {
|
||||
width: 24rem;
|
||||
}
|
||||
|
||||
.wire-popover[data-width="xl"] .wire-popover__panel {
|
||||
width: 32rem;
|
||||
}
|
||||
|
||||
.wire-popover[data-width="trigger"] .wire-popover__panel {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.wire-popover[data-placement="bottom-end"] .wire-popover__panel {
|
||||
right: 0;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.wire-popover[data-placement="top-start"] .wire-popover__panel {
|
||||
top: auto;
|
||||
bottom: calc(100% + 0.72rem);
|
||||
}
|
||||
|
||||
.wire-popover[data-placement="top-end"] .wire-popover__panel {
|
||||
top: auto;
|
||||
right: 0;
|
||||
bottom: calc(100% + 0.72rem);
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.wire-popover[data-placement="left"] .wire-popover__panel {
|
||||
top: 50%;
|
||||
right: calc(100% + 0.72rem);
|
||||
left: auto;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.wire-popover[data-placement="right"] .wire-popover__panel {
|
||||
top: 50%;
|
||||
left: calc(100% + 0.72rem);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.wire-popover[data-variant="soft"] .wire-popover__panel {
|
||||
background:
|
||||
linear-gradient(145deg, var(--popover-soft), transparent 70%),
|
||||
var(--wire-color-surface-raised);
|
||||
box-shadow: 0 18px 48px color-mix(in srgb, black 16%, transparent);
|
||||
}
|
||||
|
||||
.wire-popover[data-variant="outline"] .wire-popover__panel {
|
||||
background: var(--wire-color-surface-raised);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wire-popover[data-variant="solid"] .wire-popover__panel {
|
||||
color: var(--popover-contrast);
|
||||
background: var(--popover-accent);
|
||||
border-color: color-mix(in srgb, white 20%, transparent);
|
||||
}
|
||||
|
||||
.wire-popover__arrow {
|
||||
position: absolute;
|
||||
top: -0.35rem;
|
||||
left: 1.25rem;
|
||||
width: 0.7rem;
|
||||
height: 0.7rem;
|
||||
background: inherit;
|
||||
border-top: 1px solid color-mix(in srgb, var(--popover-accent) 18%, var(--wire-color-border));
|
||||
border-left: 1px solid color-mix(in srgb, var(--popover-accent) 18%, var(--wire-color-border));
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.wire-popover[data-placement="bottom-end"] .wire-popover__arrow {
|
||||
right: 1.25rem;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.wire-popover[data-placement="top-start"] .wire-popover__arrow,
|
||||
.wire-popover[data-placement="top-end"] .wire-popover__arrow {
|
||||
top: auto;
|
||||
bottom: -0.35rem;
|
||||
transform: rotate(225deg);
|
||||
}
|
||||
|
||||
.wire-popover[data-placement="left"] .wire-popover__arrow {
|
||||
top: 50%;
|
||||
right: -0.35rem;
|
||||
left: auto;
|
||||
transform: translateY(-50%) rotate(135deg);
|
||||
}
|
||||
|
||||
.wire-popover[data-placement="right"] .wire-popover__arrow {
|
||||
top: 50%;
|
||||
left: -0.35rem;
|
||||
transform: translateY(-50%) rotate(-45deg);
|
||||
}
|
||||
|
||||
.wire-popover__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 0.8rem;
|
||||
padding: 1rem 1rem 0.8rem;
|
||||
border-bottom: 1px solid var(--wire-color-border);
|
||||
}
|
||||
|
||||
.wire-popover__heading {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.7rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-popover__icon {
|
||||
flex: 0 0 auto;
|
||||
width: 1.05rem;
|
||||
height: 1.05rem;
|
||||
margin-top: 0.15rem;
|
||||
color: var(--popover-accent);
|
||||
}
|
||||
|
||||
.wire-popover[data-variant="solid"] .wire-popover__icon {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.wire-popover__heading-copy {
|
||||
display: grid;
|
||||
gap: 0.2rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wire-popover__heading-copy h3,
|
||||
.wire-popover__heading-copy p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wire-popover__heading-copy h3 {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.wire-popover__heading-copy p {
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.74rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.wire-popover[data-variant="solid"] .wire-popover__heading-copy p {
|
||||
color: color-mix(in srgb, currentColor 76%, transparent);
|
||||
}
|
||||
|
||||
.wire-popover__close {
|
||||
appearance: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
color: var(--wire-color-text-muted);
|
||||
background: var(--wire-color-surface-soft);
|
||||
border: 1px solid var(--wire-color-border);
|
||||
border-radius: 0.65rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wire-popover__body {
|
||||
padding: 1rem;
|
||||
color: var(--wire-color-text-muted);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.wire-popover__footer {
|
||||
padding: 0 1rem 1rem;
|
||||
}
|
||||
|
||||
.wire-popover__action {
|
||||
appearance: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.45rem;
|
||||
min-height: 2.35rem;
|
||||
padding: 0.58rem 0.8rem;
|
||||
color: var(--popover-contrast);
|
||||
background: var(--popover-accent);
|
||||
border: 0;
|
||||
border-radius: 0.7rem;
|
||||
font: inherit;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 650;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.wire-popover__panel {
|
||||
position: fixed;
|
||||
right: 0.75rem;
|
||||
bottom: 0.75rem;
|
||||
left: 0.75rem;
|
||||
top: auto;
|
||||
width: auto;
|
||||
max-width: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.wire-popover__arrow {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,71 @@
|
||||
component RangeSlider {
|
||||
outputs {
|
||||
input(payload: ({ value: number; name: string; sourceEvent: Event }) | ({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[]))
|
||||
change(payload: ({ value: number; name: string; sourceEvent: Event }) | ({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[]))
|
||||
focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "Range Slider"
|
||||
name = ""
|
||||
value = ""
|
||||
placeholder = ""
|
||||
type = "range"
|
||||
min = ""
|
||||
max = ""
|
||||
step = ""
|
||||
disabled = false
|
||||
required = false
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
id: string = ""
|
||||
name: string = ""
|
||||
label: string = "Range"
|
||||
hiddenLabel: boolean = false
|
||||
placeholder: string = ""
|
||||
variant: string = "normal"
|
||||
icon: string = ""
|
||||
iconPosition: string = "start"
|
||||
value: number = 50
|
||||
min: number = 0
|
||||
max: number = 100
|
||||
step: number = 1
|
||||
showValue: boolean = true
|
||||
showBounds: boolean = true
|
||||
showSteps: boolean = false
|
||||
marks: unknown[] = []
|
||||
helperText: string = ""
|
||||
cornerHint: string = ""
|
||||
error: string = ""
|
||||
inline: boolean = false
|
||||
readonly: boolean = false
|
||||
disabled: boolean = false
|
||||
required: boolean = false
|
||||
class: string = ""
|
||||
}
|
||||
state currentValue = value
|
||||
functions {
|
||||
shared function percent() { if (Number(max) === Number(min)) { return 0 } return (Number(currentValue) - Number(min)) / (Number(max) - Number(min)) * 100 }
|
||||
client function setValue(nextValue, sourceEvent, commit, detail, root, slider, input, outputElement, progressValue) { if (readonly || disabled) { return } currentValue = Number(nextValue); root = sourceEvent.currentTarget.closest(".wire-next--range-slider"); if (root) { slider = root.querySelector("[role=slider]"); input = root.querySelector("input[name='" + name + "']"); outputElement = root.querySelector("output"); progressValue = (currentValue - Number(min)) / (Number(max) - Number(min)) * 100; if (slider) { slider.setAttribute("aria-valuenow", String(currentValue)); slider.style.setProperty("--wire-range-progress", progressValue + "%") } if (input) { input.value = String(currentValue) } if (outputElement) { outputElement.textContent = String(currentValue) } } detail = { value: currentValue, min: min, max: max, step: step, name: name, sourceEvent: sourceEvent }; output.input(detail); if (commit) { output.change(detail) } }
|
||||
client function valueFromPointer(sourceEvent, rect, ratio) { rect = sourceEvent.currentTarget.getBoundingClientRect(); ratio = Math.max(0, Math.min(1, (sourceEvent.clientX - rect.left) / rect.width)); return Number(min) + ratio * (Number(max) - Number(min)) }
|
||||
client function beginSlide(sourceEvent, rect, ratio, rawValue) { if (readonly || disabled) { return } sourceEvent.currentTarget.setPointerCapture(sourceEvent.pointerId); rect = sourceEvent.currentTarget.getBoundingClientRect(); ratio = Math.max(0, Math.min(1, (sourceEvent.clientX - rect.left) / rect.width)); rawValue = Number(min) + ratio * (Number(max) - Number(min)); currentValue = Number(min) + Math.round((rawValue - Number(min)) / Number(step)) * Number(step); output.input({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
|
||||
client function moveSlide(sourceEvent, rect, ratio, rawValue) { if (!sourceEvent.currentTarget.hasPointerCapture(sourceEvent.pointerId)) { return } rect = sourceEvent.currentTarget.getBoundingClientRect(); ratio = Math.max(0, Math.min(1, (sourceEvent.clientX - rect.left) / rect.width)); rawValue = Number(min) + ratio * (Number(max) - Number(min)); currentValue = Number(min) + Math.round((rawValue - Number(min)) / Number(step)) * Number(step); output.input({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
|
||||
client function endSlide(sourceEvent, rect, ratio, rawValue) { rect = sourceEvent.currentTarget.getBoundingClientRect(); ratio = Math.max(0, Math.min(1, (sourceEvent.clientX - rect.left) / rect.width)); rawValue = Number(min) + ratio * (Number(max) - Number(min)); currentValue = Number(min) + Math.round((rawValue - Number(min)) / Number(step)) * Number(step); if (sourceEvent.currentTarget.hasPointerCapture(sourceEvent.pointerId)) { sourceEvent.currentTarget.releasePointerCapture(sourceEvent.pointerId) } output.change({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
|
||||
client function selectMark(sourceEvent, root, slider, input, outputElement, nextValue, progressValue, detail) { if (readonly || disabled) { return } root = sourceEvent.currentTarget.closest(".wire-next--range-slider"); slider = root.querySelector(".wire-next__custom-slider"); input = root.querySelector(".wire-next__range-value"); outputElement = root.querySelector("output"); nextValue = Number(sourceEvent.currentTarget.dataset.value); currentValue = nextValue; progressValue = (nextValue - Number(min)) / (Number(max) - Number(min)) * 100; slider.setAttribute("aria-valuenow", String(nextValue)); slider.style.setProperty("--wire-range-progress", progressValue + "%"); input.setAttribute("value", String(nextValue)); outputElement.replaceChildren(String(nextValue)); detail = { value: nextValue, min: min, max: max, step: step, name: name, sourceEvent: sourceEvent }; output.input(detail); output.change(detail) }
|
||||
client function handleKey(sourceEvent) { if (sourceEvent.key === "ArrowRight") { sourceEvent.preventDefault(); currentValue = Math.min(Number(max), Number(currentValue) + Number(step)); output.input({ value: currentValue, name: name, sourceEvent: sourceEvent }); output.change({ value: currentValue, name: name, sourceEvent: sourceEvent }) } if (sourceEvent.key === "ArrowLeft") { sourceEvent.preventDefault(); currentValue = Math.max(Number(min), Number(currentValue) - Number(step)); output.input({ value: currentValue, name: name, sourceEvent: sourceEvent }); output.change({ value: currentValue, name: name, sourceEvent: sourceEvent }) } if (sourceEvent.key === "Home") { sourceEvent.preventDefault(); currentValue = Number(min); output.change({ value: currentValue, name: name, sourceEvent: sourceEvent }) } if (sourceEvent.key === "End") { sourceEvent.preventDefault(); currentValue = Number(max); output.change({ value: currentValue, name: name, sourceEvent: sourceEvent }) } }
|
||||
client function emitFocus(nameEvent, sourceEvent) { output[nameEvent]({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
|
||||
client function decreaseValue(sourceEvent) { currentValue = Math.max(Number(min), Number(currentValue) - Number(step)); output.input({ value: currentValue, name: name, sourceEvent: sourceEvent }); output.change({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
|
||||
client function increaseValue(sourceEvent) { currentValue = Math.min(Number(max), Number(currentValue) + Number(step)); output.input({ value: currentValue, name: name, sourceEvent: sourceEvent }); output.change({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
|
||||
}
|
||||
view {
|
||||
<label class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--range-slider {class}"><span>{label}</span><input {...attrs} type="{type}" name="{name}" value="{value}" placeholder="{placeholder}" min="{min}" max="{max}" step="{step}" disabled="{disabled}" required="{required}" /></label>
|
||||
<div {...attrs} class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--range-slider {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}">
|
||||
<div class="wire-next__field-heading"><label class="{hiddenLabel ? 'wire-next__sr-only' : ''}" for="{id || name}">{label}</label>{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}</div>
|
||||
<div class="wire-next__range-control" data-variant="{variant}" data-icon-position="{iconPosition}" data-show-steps="{showSteps}">
|
||||
{#if icon}<span class="{icon}" aria-hidden="true"></span>{/if}
|
||||
<button type="button" class="wire-next__range-stepper" aria-label="Decrease {label}" disabled="{disabled || readonly}" @click="decreaseValue(event)">−</button>
|
||||
<div class="wire-next__range-track">
|
||||
<input id="{id || name}" class="wire-next__sr-only wire-next__range-value" type="number" name="{name}" value="{currentValue}" min="{min}" max="{max}" step="{step}" required="{required}" readonly />
|
||||
<div class="wire-next__custom-slider" role="slider" tabindex="{disabled ? '-1' : '0'}" aria-label="{hiddenLabel ? label : ''}" aria-valuemin="{min}" aria-valuemax="{max}" aria-valuenow="{currentValue}" aria-disabled="{disabled}" aria-readonly="{readonly}" aria-invalid="{error ? 'true' : 'false'}" style="--wire-range-progress: {percent()}%" @pointerdown="beginSlide(event)" @pointermove="moveSlide(event)" @pointerup="endSlide(event)" @pointercancel="endSlide(event)" @keydown="handleKey(event)" @focus="emitFocus('focus', event)" @blur="emitFocus('blur', event)">
|
||||
<span class="wire-next__slider-fill"></span><span class="wire-next__slider-thumb"></span>
|
||||
{#if marks.length}<span class="wire-next__slider-marks">{#each marks as mark}<button type="button" data-value="{mark.value}" style="left: {(Number(mark.value) - Number(min)) / (Number(max) - Number(min)) * 100}%" aria-label="{mark.label}" @click="selectMark(event)"></button>{/each}</span>{/if}
|
||||
</div>
|
||||
{#if showBounds}<div class="wire-next__range-bounds"><span>{min}</span><span>Step {step}</span><span>{max}</span></div>{/if}
|
||||
</div>
|
||||
{#if showValue}<output for="{id || name}">{currentValue}</output>{/if}
|
||||
<button type="button" class="wire-next__range-stepper" aria-label="Increase {label}" disabled="{disabled || readonly}" @click="increaseValue(event)">+</button>
|
||||
</div>
|
||||
{#if helperText}<small class="wire-next__field-help">{helperText}</small>{/if}<small class="wire-next__field-error" data-error="{name}">{error}</small>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
component Rating {
|
||||
outputs {
|
||||
input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Rating"
|
||||
description = ""
|
||||
items = []
|
||||
variant = "default"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
title: string = "Rating"
|
||||
description: string = ""
|
||||
items: unknown[] = []
|
||||
variant: string = "default"
|
||||
class: string = ""
|
||||
}
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--rating wire-next--variant-{variant} {class}">
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
component Scrollspy {
|
||||
outputs {
|
||||
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "Scrollspy"
|
||||
items = []
|
||||
active = ""
|
||||
orientation = "horizontal"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
label: string = "Scrollspy"
|
||||
items: unknown[] = []
|
||||
active: string = ""
|
||||
orientation: string = "horizontal"
|
||||
class: string = ""
|
||||
}
|
||||
view {
|
||||
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--scrollspy wire-next--{orientation} {class}" aria-label="{label}">
|
||||
|
||||
@@ -1,20 +1,80 @@
|
||||
component SearchBox {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "Search Box"
|
||||
name = ""
|
||||
value = ""
|
||||
placeholder = ""
|
||||
type = "search"
|
||||
min = ""
|
||||
max = ""
|
||||
step = ""
|
||||
disabled = false
|
||||
required = false
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
label: string = "Search Box"
|
||||
name: string = ""
|
||||
value: string = ""
|
||||
placeholder: string = ""
|
||||
type: string = "search"
|
||||
min: string = ""
|
||||
max: string = ""
|
||||
step: string = ""
|
||||
disabled: boolean = false
|
||||
required: boolean = false
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
state query = value
|
||||
|
||||
view {
|
||||
<label class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--search-box {class}"><span>{label}</span><input {...attrs} type="{type}" name="{name}" value="{value}" placeholder="{placeholder}" min="{min}" max="{max}" step="{step}" disabled="{disabled}" required="{required}" /></label>
|
||||
<form
|
||||
data-ui-component="SearchBox"
|
||||
role="search"
|
||||
class='w-full {class}'
|
||||
@submit='event.preventDefault(); event.currentTarget.dispatchEvent(new CustomEvent("search", { bubbles: true, detail: { value: query, name: name } }))'
|
||||
>
|
||||
<label
|
||||
class="mb-2 block text-sm font-semibold text-[var(--wire-color-text)]"
|
||||
class:sr-only='label === ""'
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
|
||||
<div class="relative flex items-center">
|
||||
<span
|
||||
class="icon-[lucide--search] pointer-events-none absolute left-4 size-5 text-[var(--wire-color-input-placeholder)]"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
|
||||
<input
|
||||
name='{name}'
|
||||
type='{type}'
|
||||
value='{query}'
|
||||
placeholder='{placeholder}'
|
||||
min='{min}'
|
||||
max='{max}'
|
||||
step='{step}'
|
||||
disabled='{disabled}'
|
||||
required='{required}'
|
||||
autocomplete="off"
|
||||
class="w-full rounded-xl border border-[var(--wire-color-input-border)] bg-[var(--wire-color-input-background)] pl-12 pr-24 text-[var(--wire-color-input-text)] outline-none transition placeholder:text-[var(--wire-color-input-placeholder)] hover:border-[var(--wire-color-input-border-hover)] focus:border-[var(--wire-color-input-border-focus)] focus:ring-2 focus:ring-[var(--wire-color-focus)] disabled:cursor-not-allowed disabled:opacity-60"
|
||||
class:h-10='size === "sm"'
|
||||
class:h-12='size === "default" || size === "md"'
|
||||
class:h-14='size === "lg"'
|
||||
@input='query = event.target.value'
|
||||
@change='query = event.target.value'
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Clear search"
|
||||
data-show='query.length > 0 && !disabled'
|
||||
class="absolute right-12 inline-flex size-8 items-center justify-center rounded-lg text-[var(--wire-color-text-muted)] transition hover:bg-[var(--wire-color-surface-soft)] hover:text-[var(--wire-color-text)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)]"
|
||||
@click='query = ""; event.currentTarget.parentElement.querySelector("input")?.focus(); event.currentTarget.dispatchEvent(new CustomEvent("clear", { bubbles: true, detail: { value: "", name: name } }))'
|
||||
>
|
||||
<span class="icon-[lucide--x] size-4" aria-hidden="true"></span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
aria-label='{label || "Search"}'
|
||||
disabled='{disabled}'
|
||||
class="absolute right-2 inline-flex size-9 items-center justify-center rounded-lg bg-[var(--wire-color-primary)] text-[var(--wire-color-on-primary)] transition hover:bg-[var(--wire-color-primary-hover)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)] disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
<span class="icon-[lucide--arrow-right] size-4" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,82 @@
|
||||
component Sidebar {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "Sidebar"
|
||||
items = []
|
||||
active = ""
|
||||
orientation = "horizontal"
|
||||
class = ""
|
||||
outputs {
|
||||
toggle(payload: { open: boolean })
|
||||
open(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
close(payload: { source: string })
|
||||
select(payload: { item: string | number | boolean | null | object; value: string | number | boolean; level: string })
|
||||
}
|
||||
|
||||
props {
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
label: string = "Sidebar"
|
||||
items: unknown[] = []
|
||||
active: string = ""
|
||||
orientation: string = "horizontal"
|
||||
mobileLabel: string = "Open navigation"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
state mobileOpen: boolean = false
|
||||
|
||||
functions {
|
||||
client function toggleSidebar() {
|
||||
mobileOpen = !mobileOpen
|
||||
output.toggle({ open: mobileOpen })
|
||||
output[mobileOpen ? "open" : "close"]({ source: "mobile" })
|
||||
}
|
||||
|
||||
client function closeSidebar() {
|
||||
mobileOpen = false
|
||||
output.close({ source: "mobile" })
|
||||
}
|
||||
|
||||
client function selectItem(item, level) {
|
||||
mobileOpen = false
|
||||
output.select({ item: item, value: item.value || "", level: level })
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--sidebar wire-next--{orientation} {class}" aria-label="{label}">
|
||||
{#each items as item}<a href="{item.href}" aria-current="{item.value === active ? 'page' : ''}">{item.label}</a>{/each}
|
||||
<slot />
|
||||
</nav>
|
||||
<div class="wire-sidebar-shell wire-next--color-{color} wire-next--size-{size} {mobileOpen ? 'is-open' : ''} {class}">
|
||||
<button type="button" class="wire-sidebar-toggle" aria-label="{mobileLabel}" aria-expanded="{mobileOpen}" @click="toggleSidebar()">
|
||||
<span class="icon-[lucide--panel-left-open]" aria-hidden="true"></span>
|
||||
<span>{label}</span>
|
||||
</button>
|
||||
<button type="button" class="wire-sidebar-backdrop" aria-label="Close navigation" @click="closeSidebar()"></button>
|
||||
<nav class="wire-next wire-next--sidebar wire-next--{orientation} wire-sidebar-panel" aria-label="{label}">
|
||||
<div class="wire-sidebar-panel__head">
|
||||
<strong>{label}</strong>
|
||||
<button type="button" aria-label="Close navigation" @click="closeSidebar()"><span class="icon-[lucide--x]" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
<div class="wire-sidebar-items">
|
||||
{#each items as item}
|
||||
{#if item.children && item.children.length}
|
||||
<details class="wire-sidebar-group" name="wire-sidebar-group">
|
||||
<summary>
|
||||
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
|
||||
<span>{item.label}</span>
|
||||
<span class="icon-[lucide--chevron-down]" aria-hidden="true"></span>
|
||||
</summary>
|
||||
<div class="wire-sidebar-group__children">
|
||||
{#each item.children as child}
|
||||
<a href="{child.href || '#'}" aria-current="{child.value === active ? 'page' : ''}" @click="selectItem(child, 2)">
|
||||
{#if child.icon}<span class="{child.icon}" aria-hidden="true"></span>{/if}
|
||||
<span>{child.label}</span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</details>
|
||||
{:else}
|
||||
<a href="{item.href || '#'}" aria-current="{item.value === active ? 'page' : ''}" @click="selectItem(item, 1)">
|
||||
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
|
||||
<span>{item.label}</span>
|
||||
</a>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<slot />
|
||||
</nav>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
component Stepper {
|
||||
outputs {
|
||||
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
previous(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
next(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
complete(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "Stepper"
|
||||
items = []
|
||||
active = ""
|
||||
orientation = "horizontal"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
label: string = "Stepper"
|
||||
items: unknown[] = []
|
||||
active: string = ""
|
||||
orientation: string = "horizontal"
|
||||
class: string = ""
|
||||
}
|
||||
view {
|
||||
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--stepper wire-next--{orientation} {class}" aria-label="{label}">
|
||||
|
||||
@@ -1,62 +1,65 @@
|
||||
component StrongPassword {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "Password"
|
||||
name = "password"
|
||||
value = ""
|
||||
placeholder = "Create a strong password"
|
||||
autocomplete = "new-password"
|
||||
minLength = 8
|
||||
specialCharactersSet = "!@#$%^&*()_+-=[]{}|;:,.<>?"
|
||||
requireLowercase = true
|
||||
requireUppercase = true
|
||||
requireNumber = true
|
||||
requireSpecialCharacter = true
|
||||
showRequirements = true
|
||||
presentation = "inline"
|
||||
hintText = "Use a unique password you do not use elsewhere."
|
||||
emptyLabel = "Enter a password"
|
||||
weakLabel = "Weak"
|
||||
fairLabel = "Fair"
|
||||
goodLabel = "Good"
|
||||
strongLabel = "Strong"
|
||||
disabled = false
|
||||
readonly = false
|
||||
required = false
|
||||
invalid = false
|
||||
validationMessage = ""
|
||||
class = ""
|
||||
@event input = function
|
||||
@event change = function
|
||||
@event strength = function
|
||||
outputs {
|
||||
input(payload: { value: string; score: number; maximumScore: number; percent: number; level: string })
|
||||
change(payload: { value: string; score: number; maximumScore: number; percent: number; level: string })
|
||||
strength(payload: { value: string; score: number; maximumScore: number; percent: number; level: string })
|
||||
}
|
||||
|
||||
props {
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
label: string = "Password"
|
||||
name: string = "password"
|
||||
value: string = ""
|
||||
placeholder: string = "Create a strong password"
|
||||
autocomplete: string = "new-password"
|
||||
minLength: number = 8
|
||||
specialCharactersSet: string = "!@#$%^&*()_+-=[]{}|;:,.<>?"
|
||||
requireLowercase: boolean = true
|
||||
requireUppercase: boolean = true
|
||||
requireNumber: boolean = true
|
||||
requireSpecialCharacter: boolean = true
|
||||
showRequirements: boolean = true
|
||||
presentation: string = "inline"
|
||||
hintText: string = "Use a unique password you do not use elsewhere."
|
||||
emptyLabel: string = "Enter a password"
|
||||
weakLabel: string = "Weak"
|
||||
fairLabel: string = "Fair"
|
||||
goodLabel: string = "Good"
|
||||
strongLabel: string = "Strong"
|
||||
disabled: boolean = false
|
||||
readonly: boolean = false
|
||||
required: boolean = false
|
||||
invalid: boolean = false
|
||||
validationMessage: string = ""
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
state password = value
|
||||
state detailsOpen = false
|
||||
state detailsOpen: boolean = false
|
||||
|
||||
functions {
|
||||
function hasMinimumLength() {
|
||||
shared function hasMinimumLength() {
|
||||
return password.length >= Number(minLength)
|
||||
}
|
||||
|
||||
function hasLowercase() {
|
||||
shared function hasLowercase() {
|
||||
return password !== password.toUpperCase()
|
||||
}
|
||||
|
||||
function hasUppercase() {
|
||||
shared function hasUppercase() {
|
||||
return password !== password.toLowerCase()
|
||||
}
|
||||
|
||||
function hasNumber() {
|
||||
shared function hasNumber() {
|
||||
return containsCharacterAt("0123456789", 0)
|
||||
}
|
||||
|
||||
function hasSpecialCharacter() {
|
||||
shared function hasSpecialCharacter() {
|
||||
return containsCharacterAt(specialCharactersSet, 0)
|
||||
}
|
||||
|
||||
function containsCharacterAt(characters, index) {
|
||||
shared function containsCharacterAt(characters, index) {
|
||||
if (index >= characters.length) {
|
||||
return false
|
||||
}
|
||||
@@ -66,19 +69,19 @@ component StrongPassword {
|
||||
return containsCharacterAt(characters, index + 1)
|
||||
}
|
||||
|
||||
function score() {
|
||||
shared function score() {
|
||||
return (hasMinimumLength() ? 1 : 0) + (requireLowercase && hasLowercase() ? 1 : 0) + (requireUppercase && hasUppercase() ? 1 : 0) + (requireNumber && hasNumber() ? 1 : 0) + (requireSpecialCharacter && hasSpecialCharacter() ? 1 : 0)
|
||||
}
|
||||
|
||||
function maximumScore() {
|
||||
shared function maximumScore() {
|
||||
return 1 + (requireLowercase ? 1 : 0) + (requireUppercase ? 1 : 0) + (requireNumber ? 1 : 0) + (requireSpecialCharacter ? 1 : 0)
|
||||
}
|
||||
|
||||
function strengthPercent() {
|
||||
shared function strengthPercent() {
|
||||
return Math.round(score() / maximumScore() * 100)
|
||||
}
|
||||
|
||||
function strengthLevel() {
|
||||
shared function strengthLevel() {
|
||||
if (!password) {
|
||||
return "empty"
|
||||
}
|
||||
@@ -94,7 +97,7 @@ component StrongPassword {
|
||||
return "strong"
|
||||
}
|
||||
|
||||
function strengthLabel() {
|
||||
shared function strengthLabel() {
|
||||
if (strengthLevel() === "weak") {
|
||||
return weakLabel
|
||||
}
|
||||
@@ -137,8 +140,8 @@ component StrongPassword {
|
||||
aria-describedby="{validationMessage ? name + '-validation' : hintText ? name + '-hint' : ''}"
|
||||
@focus="detailsOpen = true"
|
||||
@blur="detailsOpen = false"
|
||||
@input="password = event.target.value; $emit('input', { value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() }); $emit('strength', { value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() })"
|
||||
@change="$emit('change', { value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() })"
|
||||
@input="password = event.target.value; output.input({ value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() }); output.strength({ value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() })"
|
||||
@change="output.change({ value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() })"
|
||||
/>
|
||||
|
||||
{#if presentation === "popover"}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
component StyledIcon {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Styled Icon"
|
||||
description = ""
|
||||
items = []
|
||||
variant = "default"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
title: string = "Styled Icon"
|
||||
description: string = ""
|
||||
items: unknown[] = []
|
||||
variant: string = "default"
|
||||
class: string = ""
|
||||
}
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--styled-icon wire-next--variant-{variant} {class}">
|
||||
|
||||
@@ -1,17 +1,90 @@
|
||||
component Tabs {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "Tabs"
|
||||
items = []
|
||||
active = ""
|
||||
orientation = "horizontal"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
label: string = "Tabs"
|
||||
items: unknown[] = []
|
||||
active: string = ""
|
||||
orientation: string = "horizontal"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
state activeValue = active || (items[0] ? (items[0].value || items[0].id || "0") : "")
|
||||
|
||||
view {
|
||||
<nav class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--tabs wire-next--{orientation} {class}" aria-label="{label}">
|
||||
{#each items as item}<a href="{item.href}" aria-current="{item.value === active ? 'page' : ''}">{item.label}</a>{/each}
|
||||
<slot />
|
||||
</nav>
|
||||
<section
|
||||
data-ui-component="Tabs"
|
||||
class='w-full {class}'
|
||||
class:flex='orientation === "vertical"'
|
||||
class:items-start='orientation === "vertical"'
|
||||
class:gap-6='orientation === "vertical"'
|
||||
>
|
||||
<div
|
||||
role="tablist"
|
||||
aria-label='{label}'
|
||||
aria-orientation='{orientation}'
|
||||
class="flex min-w-0 gap-1 overflow-x-auto rounded-xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-soft)] p-1"
|
||||
class:flex-col='orientation === "vertical"'
|
||||
class:w-56='orientation === "vertical"'
|
||||
class:shrink-0='orientation === "vertical"'
|
||||
>
|
||||
{#each items as item, index}
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
id='tab-{item.value || item.id || index}'
|
||||
aria-selected='{activeValue === (item.value || item.id || String(index))}'
|
||||
aria-controls='panel-{item.value || item.id || index}'
|
||||
tabindex='{activeValue === (item.value || item.id || String(index)) ? "0" : "-1"}'
|
||||
disabled='{item.disabled || false}'
|
||||
class="inline-flex min-w-max flex-1 items-center justify-center gap-2 rounded-lg px-4 py-2.5 text-sm font-semibold outline-none transition disabled:cursor-not-allowed disabled:opacity-50"
|
||||
class:bg-[var(--wire-color-surface-raised)]='activeValue === (item.value || item.id || String(index))'
|
||||
class:text-[var(--wire-color-primary)]='activeValue === (item.value || item.id || String(index))'
|
||||
class:shadow-sm='activeValue === (item.value || item.id || String(index))'
|
||||
class:text-[var(--wire-color-text-muted)]='activeValue !== (item.value || item.id || String(index))'
|
||||
class:hover:text-[var(--wire-color-text)]='activeValue !== (item.value || item.id || String(index))'
|
||||
class:justify-start='orientation === "vertical"'
|
||||
class:px-3='size === "sm"'
|
||||
class:py-2='size === "sm"'
|
||||
class:px-5='size === "lg"'
|
||||
class:py-3='size === "lg"'
|
||||
@click='activeValue = item.value || item.id || String(index); event.currentTarget.dispatchEvent(new CustomEvent("change", { bubbles: true, detail: { item: item, index: index, value: activeValue } })); event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: item, index: index, value: activeValue } }))'
|
||||
>
|
||||
{#if item.icon}
|
||||
<span class='{item.icon + " size-4"}' aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span>{item.label || item.title}</span>
|
||||
{#if item.badge}
|
||||
<span class="rounded-full bg-[var(--wire-color-primary-soft)] px-2 py-0.5 text-xs text-[var(--wire-color-primary)]">{item.badge}</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 flex-1 pt-4" class:pt-0='orientation === "vertical"'>
|
||||
{#each items as item, index}
|
||||
<div
|
||||
role="tabpanel"
|
||||
id='panel-{item.value || item.id || index}'
|
||||
aria-labelledby='tab-{item.value || item.id || index}'
|
||||
tabindex="0"
|
||||
data-show='activeValue === (item.value || item.id || String(index))'
|
||||
class="rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)]"
|
||||
>
|
||||
{#if item.title && item.title !== item.label}
|
||||
<h3 class="text-lg font-bold text-[var(--wire-color-text)]">{item.title}</h3>
|
||||
{/if}
|
||||
{#if item.description}
|
||||
<p class="mt-2 leading-7 text-[var(--wire-color-text-muted)]">{item.description}</p>
|
||||
{/if}
|
||||
{#if item.content}
|
||||
<div class="mt-4 text-[var(--wire-color-text)]">{item.content}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<slot></slot>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,66 @@
|
||||
component TimePicker {
|
||||
outputs {
|
||||
input(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
|
||||
change(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
|
||||
focus(payload: { value: string; name: string; sourceEvent: Event })
|
||||
blur(payload: { value: string; name: string; sourceEvent: Event })
|
||||
open(payload: { value: string; name: string; sourceEvent: Event })
|
||||
close(payload: { value: string; name: string; sourceEvent: Event })
|
||||
invalid(payload: { name: string; message: string; sourceEvent: Event })
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "Time Picker"
|
||||
name = ""
|
||||
value = ""
|
||||
placeholder = ""
|
||||
type = "time"
|
||||
min = ""
|
||||
max = ""
|
||||
step = ""
|
||||
disabled = false
|
||||
required = false
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
id: string = ""
|
||||
name: string = ""
|
||||
label: string = "Time"
|
||||
hiddenLabel: boolean = false
|
||||
value: string = ""
|
||||
placeholder: string = ""
|
||||
variant: string = "normal"
|
||||
icon: string = "icon-[lucide--clock-3]"
|
||||
iconPosition: string = "end"
|
||||
helperText: string = ""
|
||||
cornerHint: string = ""
|
||||
error: string = ""
|
||||
inline: boolean = false
|
||||
min: string = ""
|
||||
max: string = ""
|
||||
step: string = ""
|
||||
format: string = "24"
|
||||
minuteStep: number = 5
|
||||
hours = ["00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23"]
|
||||
minutes = ["00", "05", "10", "15", "20", "25", "30", "35", "40", "45", "50", "55"]
|
||||
readonly: boolean = false
|
||||
disabled: boolean = false
|
||||
required: boolean = false
|
||||
class: string = ""
|
||||
}
|
||||
state currentValue = value
|
||||
state selectedHour = value ? value.split(":")[0] : "00"
|
||||
state selectedMinute = value ? value.split(":")[1] : "00"
|
||||
state expanded: boolean = false
|
||||
functions {
|
||||
client function commit(part, nextValue, sourceEvent, next, detail) { if (part === "hour") { selectedHour = String(nextValue).padStart(2, "0") } if (part === "minute") { selectedMinute = String(nextValue).padStart(2, "0") } next = selectedHour + ":" + selectedMinute; if ((min && next < min) || (max && next > max)) { return } currentValue = next; detail = { value: currentValue, hour: selectedHour, minute: selectedMinute, name: name, sourceEvent: sourceEvent }; output.input(detail); output.change(detail) }
|
||||
client function selectTimePart(sourceEvent, root, input, parts, hourValue, minuteValue, next, trigger, detail) { root = sourceEvent.currentTarget.closest(".wire-next--time-picker"); input = root.querySelector(".wire-next__time-value"); parts = String(input.value || value || "00:00").split(":"); hourValue = sourceEvent.currentTarget.dataset.part === "hour" ? sourceEvent.currentTarget.dataset.value : parts[0]; minuteValue = sourceEvent.currentTarget.dataset.part === "minute" ? sourceEvent.currentTarget.dataset.value : parts[1]; next = hourValue + ":" + minuteValue; if ((min && next < min) || (max && next > max)) { return } currentValue = next; input.setAttribute("value", next); trigger = root.querySelector(".wire-next__time-trigger span"); if (trigger) { trigger.replaceChildren(next) } sourceEvent.currentTarget.setAttribute("aria-pressed", "true"); detail = { value: next, hour: hourValue, minute: minuteValue, name: name, sourceEvent: sourceEvent }; output.input(detail); output.change(detail) }
|
||||
client function openPicker(sourceEvent) { expanded = true; output.open({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
|
||||
client function closePicker(sourceEvent) { expanded = false; output.close({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
|
||||
client function handleFocus(sourceEvent) { output.focus({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
|
||||
client function handleBlur(sourceEvent) { output.blur({ value: currentValue, name: name, sourceEvent: sourceEvent }) }
|
||||
client function handleInvalid(sourceEvent) { output.invalid({ name: name, message: sourceEvent.currentTarget.validationMessage, sourceEvent: sourceEvent }) }
|
||||
}
|
||||
view {
|
||||
<label class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--time-picker {class}"><span>{label}</span><input {...attrs} type="{type}" name="{name}" value="{value}" placeholder="{placeholder}" min="{min}" max="{max}" step="{step}" disabled="{disabled}" required="{required}" /></label>
|
||||
<div {...attrs} class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--time-picker {class}" data-variant="{variant}" data-inline="{inline}" data-floating="{variant === 'floating'}" data-invalid="{error ? 'true' : 'false'}" data-expanded="{expanded}">
|
||||
<div class="wire-next__field-heading"><label class="{hiddenLabel ? 'wire-next__sr-only' : ''}" for="{id || name}">{label}</label>{#if cornerHint}<span class="wire-next__field-hint">{cornerHint}</span>{/if}</div>
|
||||
<div class="wire-next__field-control" data-icon-position="{iconPosition}">
|
||||
{#if icon}<span class="{icon} wire-next__field-icon" aria-hidden="true"></span>{/if}
|
||||
<input id="{id || name}" class="wire-next__sr-only wire-next__time-value" type="text" name="{name}" value="{currentValue}" required="{required}" readonly aria-hidden="true" tabindex="-1" @invalid="handleInvalid(event)" />
|
||||
<button type="button" class="wire-next__time-trigger" disabled="{disabled || readonly}" aria-haspopup="dialog" aria-expanded="{expanded}" @click="openPicker(event)" @focus="handleFocus(event)" @blur="handleBlur(event)"><span>{currentValue || placeholder || "Select time"}</span><i class="{icon}" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<div class="wire-next__time-panel" role="dialog" aria-label="{label}"><div><strong>Hour</strong><div class="wire-next__time-options">{#each hours as hour}<button type="button" data-part="hour" data-value="{hour}" aria-pressed="{String(hour) === selectedHour}" @click="selectTimePart(event)">{hour}</button>{/each}</div></div><div><strong>Minute</strong><div class="wire-next__time-options">{#each minutes as minute}<button type="button" data-part="minute" data-value="{minute}" aria-pressed="{String(minute) === selectedMinute}" @click="selectTimePart(event)">{minute}</button>{/each}</div></div><button type="button" class="wire-next__time-done" @click="closePicker(event)">Done</button></div>
|
||||
{#if helperText}<small class="wire-next__field-help">{helperText}</small>{/if}
|
||||
<small class="wire-next__field-error" data-error="{name}">{error}</small>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,83 @@
|
||||
component Timeline {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Timeline"
|
||||
description = ""
|
||||
items = []
|
||||
variant = "default"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
title: string = "Timeline"
|
||||
description: string = ""
|
||||
items: unknown[] = []
|
||||
variant: string = "default"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--timeline wire-next--variant-{variant} {class}">
|
||||
{#if title}<strong>{title}</strong>{/if}
|
||||
{#if description}<p>{description}</p>{/if}
|
||||
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
|
||||
<slot />
|
||||
<section
|
||||
data-ui-component="Timeline"
|
||||
aria-label='{title}'
|
||||
class='w-full {class}'
|
||||
>
|
||||
{#if title || description}
|
||||
<header class="mb-6">
|
||||
{#if title}
|
||||
<h3 class="text-xl font-bold text-[var(--wire-color-text)]">{title}</h3>
|
||||
{/if}
|
||||
{#if description}
|
||||
<p class="mt-2 max-w-2xl leading-7 text-[var(--wire-color-text-muted)]">{description}</p>
|
||||
{/if}
|
||||
</header>
|
||||
{/if}
|
||||
|
||||
<ol class="relative ml-4 border-l border-[var(--wire-color-border)]">
|
||||
{#each items as item, index}
|
||||
<li
|
||||
class="relative pb-8 pl-8 last:pb-0"
|
||||
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: item, index: index } }))'
|
||||
>
|
||||
<span
|
||||
class="absolute -left-[1.05rem] top-0 flex size-8 items-center justify-center rounded-full border-4 border-[var(--wire-color-background)] bg-[var(--wire-color-primary)] text-[var(--wire-color-on-primary)] shadow-sm"
|
||||
class:bg-[var(--wire-color-success)]='item.status === "complete" || item.status === "success"'
|
||||
class:bg-[var(--wire-color-warning)]='item.status === "warning" || item.status === "pending"'
|
||||
class:bg-[var(--wire-color-danger)]='item.status === "danger" || item.status === "failed"'
|
||||
>
|
||||
<span class='{item.icon || "icon-[lucide--circle]"}' aria-hidden="true"></span>
|
||||
</span>
|
||||
|
||||
<article
|
||||
class="rounded-2xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-raised)] p-5 shadow-sm"
|
||||
class:border-transparent='variant === "minimal"'
|
||||
class:bg-transparent='variant === "minimal"'
|
||||
class:p-0='variant === "minimal"'
|
||||
class:shadow-none='variant === "minimal"'
|
||||
>
|
||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<h4 class="font-bold text-[var(--wire-color-text)]">{item.title || item.label}</h4>
|
||||
{#if item.subtitle}
|
||||
<p class="mt-1 text-sm font-medium text-[var(--wire-color-primary)]">{item.subtitle}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{#if item.date || item.meta}
|
||||
<time class="shrink-0 text-sm text-[var(--wire-color-text-muted)]">{item.date || item.meta}</time>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if item.description}
|
||||
<p class="mt-3 text-sm leading-6 text-[var(--wire-color-text-muted)]">{item.description}</p>
|
||||
{/if}
|
||||
|
||||
{#if item.href}
|
||||
<a href='{item.href}' class="mt-4 inline-flex items-center gap-2 text-sm font-semibold text-[var(--wire-color-primary)] hover:text-[var(--wire-color-primary-hover)]">
|
||||
<span>{item.actionLabel || "View details"}</span>
|
||||
<span class="icon-[lucide--arrow-right] size-4" aria-hidden="true"></span>
|
||||
</a>
|
||||
{/if}
|
||||
</article>
|
||||
</li>
|
||||
{:empty}
|
||||
<li class="pl-8 text-sm text-[var(--wire-color-text-muted)]">No timeline items available.</li>
|
||||
{/each}
|
||||
</ol>
|
||||
|
||||
<slot></slot>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
component Toast {
|
||||
outputs {
|
||||
dismiss(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
action(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Toast"
|
||||
description = ""
|
||||
items = []
|
||||
variant = "default"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
title: string = "Toast"
|
||||
description: string = ""
|
||||
items: unknown[] = []
|
||||
variant: string = "default"
|
||||
class: string = ""
|
||||
}
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--toast wire-next--variant-{variant} {class}">
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
component ToastNotifications {
|
||||
outputs {
|
||||
add(payload: { sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])
|
||||
dismiss(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
clear(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
action(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Toast Notifications"
|
||||
description = ""
|
||||
items = []
|
||||
variant = "default"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
title: string = "Toast Notifications"
|
||||
description: string = ""
|
||||
items: unknown[] = []
|
||||
variant: string = "default"
|
||||
class: string = ""
|
||||
}
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--toast-notifications wire-next--variant-{variant} {class}">
|
||||
|
||||
@@ -1,20 +1,300 @@
|
||||
component ToggleCount {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "Toggle Count"
|
||||
name = ""
|
||||
value = ""
|
||||
placeholder = ""
|
||||
type = "text"
|
||||
min = ""
|
||||
max = ""
|
||||
step = ""
|
||||
disabled = false
|
||||
required = false
|
||||
class = ""
|
||||
outputs {
|
||||
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
toggle(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
}
|
||||
|
||||
props {
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
variant: string = "segmented"
|
||||
class: string = ""
|
||||
|
||||
name: string = "billing-cycle"
|
||||
value: string = "monthly"
|
||||
firstValue: string = "monthly"
|
||||
firstLabel: string = "Monthly"
|
||||
secondValue: string = "annual"
|
||||
secondLabel: string = "Annual"
|
||||
ariaLabel: string = "Billing frequency"
|
||||
|
||||
items: unknown[] = []
|
||||
currency: string = "$"
|
||||
suffix: string = ""
|
||||
firstValueKey: string = "monthly"
|
||||
secondValueKey: string = "annual"
|
||||
emptyValue: string = "—"
|
||||
|
||||
align: string = "end"
|
||||
fullWidth: boolean = true
|
||||
disabled: boolean = false
|
||||
animate: boolean = true
|
||||
animationDuration: number = 450
|
||||
animationSteps: number = 18
|
||||
|
||||
}
|
||||
|
||||
state selectedValue = value
|
||||
|
||||
functions {
|
||||
shared function isFirstSelected() {
|
||||
return selectedValue === firstValue
|
||||
}
|
||||
|
||||
shared function isSecondSelected() {
|
||||
return selectedValue === secondValue
|
||||
}
|
||||
|
||||
shared function displayValue(item) {
|
||||
if (isSecondSelected()) {
|
||||
return item[secondValueKey] !== undefined
|
||||
? item[secondValueKey]
|
||||
: emptyValue
|
||||
}
|
||||
|
||||
return item[firstValueKey] !== undefined
|
||||
? item[firstValueKey]
|
||||
: emptyValue
|
||||
}
|
||||
|
||||
client function dispatchToggleEvent(sourceEvent, previousValue, root, customEvent) {
|
||||
root = sourceEvent.currentTarget.closest("[data-wrn-toggle-count]")
|
||||
|
||||
if (!root) {
|
||||
return
|
||||
}
|
||||
|
||||
customEvent = document.createEvent("CustomEvent")
|
||||
customEvent.initCustomEvent("change", true, false, {
|
||||
component: "ToggleCount",
|
||||
name: name,
|
||||
value: selectedValue,
|
||||
previousValue: previousValue,
|
||||
firstValue: firstValue,
|
||||
secondValue: secondValue
|
||||
})
|
||||
root.dispatchEvent(customEvent)
|
||||
|
||||
customEvent = document.createEvent("CustomEvent")
|
||||
customEvent.initCustomEvent("toggle", true, false, {
|
||||
component: "ToggleCount",
|
||||
name: name,
|
||||
value: selectedValue,
|
||||
previousValue: previousValue,
|
||||
firstValue: firstValue,
|
||||
secondValue: secondValue
|
||||
})
|
||||
root.dispatchEvent(customEvent)
|
||||
}
|
||||
|
||||
client function selectValue(sourceEvent, nextValue, previousValue) {
|
||||
if (disabled || selectedValue === nextValue) {
|
||||
return
|
||||
}
|
||||
|
||||
previousValue = selectedValue
|
||||
selectedValue = nextValue
|
||||
|
||||
if (
|
||||
animate &&
|
||||
!window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
||||
) {
|
||||
animateDisplayedValues(sourceEvent, previousValue)
|
||||
}
|
||||
|
||||
dispatchToggleEvent(sourceEvent, previousValue)
|
||||
}
|
||||
|
||||
client function animateDisplayedValues(sourceEvent, previousValue, root, nodes, animationToken) {
|
||||
root = sourceEvent.currentTarget.closest("[data-wrn-toggle-count]")
|
||||
|
||||
if (!root) {
|
||||
return
|
||||
}
|
||||
|
||||
animationToken = String(Date.now()) + ":" + String(selectedValue)
|
||||
root.setAttribute("data-animation-token", animationToken)
|
||||
nodes = root.querySelectorAll("[data-toggle-count-value]")
|
||||
animateValueAt(nodes, 0, previousValue, root, animationToken)
|
||||
}
|
||||
|
||||
client function animateValueAt(
|
||||
nodes,
|
||||
index,
|
||||
previousValue,
|
||||
root,
|
||||
animationToken,
|
||||
node,
|
||||
fromValue,
|
||||
toValue
|
||||
) {
|
||||
if (index >= nodes.length) {
|
||||
return
|
||||
}
|
||||
|
||||
node = nodes[index]
|
||||
fromValue = Number(
|
||||
previousValue === secondValue
|
||||
? node.getAttribute("data-second-value")
|
||||
: node.getAttribute("data-first-value")
|
||||
)
|
||||
toValue = Number(
|
||||
selectedValue === secondValue
|
||||
? node.getAttribute("data-second-value")
|
||||
: node.getAttribute("data-first-value")
|
||||
)
|
||||
|
||||
if (!Number.isNaN(fromValue) && !Number.isNaN(toValue)) {
|
||||
node.replaceChildren(String(fromValue))
|
||||
scheduleValueFrame(
|
||||
node,
|
||||
fromValue,
|
||||
toValue,
|
||||
1,
|
||||
Math.max(1, Number(animationSteps) || 1),
|
||||
Math.max(1, Number(animationDuration) || 1),
|
||||
root,
|
||||
animationToken
|
||||
)
|
||||
}
|
||||
|
||||
animateValueAt(
|
||||
nodes,
|
||||
index + 1,
|
||||
previousValue,
|
||||
root,
|
||||
animationToken
|
||||
)
|
||||
}
|
||||
|
||||
client function scheduleValueFrame(
|
||||
node,
|
||||
fromValue,
|
||||
toValue,
|
||||
frame,
|
||||
totalFrames,
|
||||
duration,
|
||||
root,
|
||||
animationToken,
|
||||
nextValue
|
||||
) {
|
||||
if (frame > totalFrames) {
|
||||
return
|
||||
}
|
||||
|
||||
nextValue = Math.round(
|
||||
fromValue +
|
||||
(toValue - fromValue) * (frame / totalFrames)
|
||||
)
|
||||
|
||||
setTimeout(
|
||||
applyAnimatedValue,
|
||||
Math.max(1, duration * frame / totalFrames),
|
||||
node,
|
||||
nextValue,
|
||||
root,
|
||||
animationToken
|
||||
)
|
||||
|
||||
scheduleValueFrame(
|
||||
node,
|
||||
fromValue,
|
||||
toValue,
|
||||
frame + 1,
|
||||
totalFrames,
|
||||
duration,
|
||||
root,
|
||||
animationToken
|
||||
)
|
||||
}
|
||||
|
||||
client function applyAnimatedValue(
|
||||
node,
|
||||
nextValue,
|
||||
root,
|
||||
animationToken
|
||||
) {
|
||||
if (
|
||||
root &&
|
||||
root.getAttribute("data-animation-token") !== animationToken
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
node.replaceChildren(String(nextValue))
|
||||
}
|
||||
|
||||
client function toggleValue(sourceEvent) {
|
||||
selectValue(
|
||||
sourceEvent,
|
||||
isFirstSelected() ? secondValue : firstValue
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<label class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--field wire-next--toggle-count {class}"><span>{label}</span><input {...attrs} type="{type}" name="{name}" value="{value}" placeholder="{placeholder}" min="{min}" max="{max}" step="{step}" disabled="{disabled}" required="{required}" /></label>
|
||||
<section
|
||||
{...attrs}
|
||||
data-wrn-toggle-count
|
||||
data-variant="{variant}"
|
||||
data-value="{selectedValue}"
|
||||
data-disabled="{disabled ? 'true' : 'false'}"
|
||||
class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--toggle-count wire-next--toggle-count-{variant} {fullWidth ? 'wire-next--toggle-count-full' : ''} {disabled ? 'wire-next--disabled' : ''} {class}"
|
||||
>
|
||||
<input type="hidden" name="{name}" value="{selectedValue}" />
|
||||
|
||||
<div class="wire-next__toggle-count-control wire-next__toggle-count-control--{align}">
|
||||
{#if variant === "switch"}
|
||||
<span data-selected="{isFirstSelected() ? 'true' : 'false'}">{firstLabel}</span>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-label="{ariaLabel}"
|
||||
aria-checked="{isSecondSelected() ? 'true' : 'false'}"
|
||||
disabled="{disabled}"
|
||||
@click="toggleValue(event)"
|
||||
class="wire-next__toggle-count-switch"
|
||||
>
|
||||
<span aria-hidden="true"></span>
|
||||
</button>
|
||||
<span data-selected="{isSecondSelected() ? 'true' : 'false'}">{secondLabel}</span>
|
||||
{:else}
|
||||
<div class="wire-next__toggle-count-segmented" role="group" aria-label="{ariaLabel}">
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed="{isFirstSelected() ? 'true' : 'false'}"
|
||||
disabled="{disabled}"
|
||||
@click="selectValue(event, firstValue)"
|
||||
>{firstLabel}</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed="{isSecondSelected() ? 'true' : 'false'}"
|
||||
disabled="{disabled}"
|
||||
@click="selectValue(event, secondValue)"
|
||||
>{secondLabel}</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if items.length > 0}
|
||||
<div class="wire-next__toggle-count-items">
|
||||
{#each items as item}
|
||||
<article>
|
||||
<span>{item.label || item.name}</span>
|
||||
<strong>
|
||||
{#if currency}<small>{currency}</small>{/if}
|
||||
<span
|
||||
data-toggle-count-value
|
||||
data-first-value="{item[firstValueKey]}"
|
||||
data-second-value="{item[secondValueKey]}"
|
||||
>{displayValue(item)}</span>
|
||||
{#if suffix}<small>{suffix}</small>{/if}
|
||||
</strong>
|
||||
{#if item.description}<small>{item.description}</small>{/if}
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +1,52 @@
|
||||
component TogglePassword {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "Password"
|
||||
name = "password"
|
||||
value = ""
|
||||
placeholder = "Enter your password"
|
||||
autocomplete = "current-password"
|
||||
minlength = ""
|
||||
maxlength = ""
|
||||
pattern = "(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9]).{8,}"
|
||||
fields = []
|
||||
visible = false
|
||||
toggleable = true
|
||||
toggleMode = "button"
|
||||
checkboxLabel = "Show password"
|
||||
showLabel = "Show password"
|
||||
hideLabel = "Hide password"
|
||||
disabled = false
|
||||
readonly = false
|
||||
required = false
|
||||
invalid = false
|
||||
helpText = ""
|
||||
validationMessage = ""
|
||||
class = ""
|
||||
@event input = function
|
||||
@event change = function
|
||||
@event toggle = function
|
||||
outputs {
|
||||
input(payload: { name?: string; value: string | number | boolean | null | object; visible: boolean })
|
||||
change(payload: { name?: string; value: string | number | boolean | null | object; visible: boolean })
|
||||
toggle(payload: { visible: boolean })
|
||||
}
|
||||
|
||||
props {
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
label: string = "Password"
|
||||
name: string = "password"
|
||||
value: string = ""
|
||||
placeholder: string = "Enter your password"
|
||||
autocomplete: string = "current-password"
|
||||
minlength: string = ""
|
||||
maxlength: string = ""
|
||||
pattern: string = "(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9]).{8,}"
|
||||
fields: unknown[] = []
|
||||
visible: boolean = false
|
||||
toggleable: boolean = true
|
||||
toggleMode: string = "button"
|
||||
checkboxLabel: string = "Show password"
|
||||
showLabel: string = "Show password"
|
||||
hideLabel: string = "Hide password"
|
||||
disabled: boolean = false
|
||||
readonly: boolean = false
|
||||
required: boolean = false
|
||||
invalid: boolean = false
|
||||
helpText: string = ""
|
||||
validationMessage: string = ""
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
state revealed = visible
|
||||
|
||||
functions {
|
||||
function toggleVisibility() {
|
||||
shared function toggleVisibility() {
|
||||
if (disabled || readonly || !toggleable) {
|
||||
return
|
||||
}
|
||||
revealed = !revealed
|
||||
}
|
||||
|
||||
function fieldId(field, index) {
|
||||
shared function fieldId(field, index) {
|
||||
return field.id || field.name || name + "-" + index
|
||||
}
|
||||
|
||||
function fieldName(field, index) {
|
||||
shared function fieldName(field, index) {
|
||||
return field.name || name + "-" + index
|
||||
}
|
||||
}
|
||||
@@ -74,8 +77,8 @@ component TogglePassword {
|
||||
readonly="{readonly || field.readonly}"
|
||||
required="{required || field.required}"
|
||||
aria-invalid="{invalid ? 'true' : 'false'}"
|
||||
@input="$emit('input', { name: event.target.name, value: event.target.value, visible: revealed })"
|
||||
@change="$emit('change', { name: event.target.name, value: event.target.value, visible: revealed })"
|
||||
@input="output.input({ name: event.target.name, value: event.target.value, visible: revealed })"
|
||||
@change="output.change({ name: event.target.name, value: event.target.value, visible: revealed })"
|
||||
/>
|
||||
{#if toggleable && toggleMode === "button"}
|
||||
<button
|
||||
@@ -85,7 +88,7 @@ component TogglePassword {
|
||||
title="{revealed ? hideLabel : showLabel}"
|
||||
aria-pressed="{revealed ? 'true' : 'false'}"
|
||||
disabled="{disabled || readonly}"
|
||||
@click="toggleVisibility(); $emit('toggle', { visible: revealed })"
|
||||
@click="toggleVisibility(); output.toggle({ visible: revealed })"
|
||||
>
|
||||
<span class="wire-next__password-icon wire-next__password-icon--show" aria-hidden="true"></span>
|
||||
</button>
|
||||
@@ -113,8 +116,8 @@ component TogglePassword {
|
||||
required="{required}"
|
||||
aria-invalid="{invalid ? 'true' : 'false'}"
|
||||
aria-describedby="{validationMessage ? name + '-validation' : helpText ? name + '-help' : ''}"
|
||||
@input="$emit('input', { value: event.target.value, visible: revealed })"
|
||||
@change="$emit('change', { value: event.target.value, visible: revealed })"
|
||||
@input="output.input({ value: event.target.value, visible: revealed })"
|
||||
@change="output.change({ value: event.target.value, visible: revealed })"
|
||||
/>
|
||||
{#if toggleable && toggleMode === "button"}
|
||||
<button
|
||||
@@ -124,7 +127,7 @@ component TogglePassword {
|
||||
title="{revealed ? hideLabel : showLabel}"
|
||||
aria-pressed="{revealed ? 'true' : 'false'}"
|
||||
disabled="{disabled || readonly}"
|
||||
@click="toggleVisibility(); $emit('toggle', { visible: revealed })"
|
||||
@click="toggleVisibility(); output.toggle({ visible: revealed })"
|
||||
>
|
||||
<span class="wire-next__password-icon wire-next__password-icon--show" aria-hidden="true"></span>
|
||||
</button>
|
||||
@@ -137,7 +140,7 @@ component TogglePassword {
|
||||
type="checkbox"
|
||||
checked="{revealed}"
|
||||
disabled="{disabled || readonly}"
|
||||
@change="revealed = event.target.checked; $emit('toggle', { visible: revealed })"
|
||||
@change="revealed = event.target.checked; output.toggle({ visible: revealed })"
|
||||
/>
|
||||
<span>{checkboxLabel}</span>
|
||||
</label>
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
component TreeView {
|
||||
outputs {
|
||||
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
toggle(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
expand(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
collapse(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Tree View"
|
||||
description = ""
|
||||
items = []
|
||||
variant = "default"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
title: string = "Tree View"
|
||||
description: string = ""
|
||||
items: unknown[] = []
|
||||
variant: string = "default"
|
||||
class: string = ""
|
||||
}
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--tree-view wire-next--variant-{variant} {class}">
|
||||
|
||||
@@ -1,13 +1,75 @@
|
||||
component Typography {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
columns = 2
|
||||
gap = "md"
|
||||
maxWidth = "xl"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
columns: number = 2
|
||||
gap: string = "md"
|
||||
maxWidth: string = "xl"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
view {
|
||||
<article class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--typography wire-next--gap-{gap} wire-next--columns-{columns} wire-next--max-{maxWidth} {class}"><slot /></article>
|
||||
<div
|
||||
data-ui-component="Typography"
|
||||
class='wire-typography text-[var(--wire-color-text)] {class}'
|
||||
class:max-w-3xl='maxWidth === "md"'
|
||||
class:max-w-5xl='maxWidth === "lg"'
|
||||
class:max-w-7xl='maxWidth === "xl"'
|
||||
class:max-w-none='maxWidth === "full"'
|
||||
class:text-sm='size === "sm"'
|
||||
class:text-base='size === "default" || size === "md"'
|
||||
class:text-lg='size === "lg"'
|
||||
class:columns-1='columns === 1'
|
||||
class:md:columns-2='columns === 2'
|
||||
class:lg:columns-3='columns === 3'
|
||||
class:gap-4='gap === "sm"'
|
||||
class:gap-8='gap === "md"'
|
||||
class:gap-12='gap === "lg"'
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
}
|
||||
|
||||
style {
|
||||
.wire-typography :where(h1, h2, h3, h4) {
|
||||
color: var(--wire-color-text);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.2;
|
||||
break-after: avoid;
|
||||
}
|
||||
|
||||
.wire-typography :where(h1) { font-size: clamp(2rem, 4vw, 3.5rem); margin: 0 0 1.5rem; }
|
||||
.wire-typography :where(h2) { font-size: clamp(1.5rem, 3vw, 2.25rem); margin: 2.5rem 0 1rem; }
|
||||
.wire-typography :where(h3) { font-size: 1.35rem; margin: 2rem 0 0.75rem; }
|
||||
.wire-typography :where(p, ul, ol, blockquote, pre, table) { margin: 1rem 0; }
|
||||
.wire-typography :where(p, li) { color: var(--wire-color-text-muted); line-height: 1.8; }
|
||||
.wire-typography :where(a) { color: var(--wire-color-primary); font-weight: 600; text-underline-offset: 0.2em; }
|
||||
.wire-typography :where(a:hover) { color: var(--wire-color-primary-hover); text-decoration: underline; }
|
||||
.wire-typography :where(ul, ol) { padding-left: 1.4rem; }
|
||||
.wire-typography :where(ul) { list-style: disc; }
|
||||
.wire-typography :where(ol) { list-style: decimal; }
|
||||
.wire-typography :where(blockquote) {
|
||||
border-left: 4px solid var(--wire-color-primary);
|
||||
background: var(--wire-color-primary-soft);
|
||||
border-radius: 0 0.75rem 0.75rem 0;
|
||||
padding: 1rem 1.25rem;
|
||||
color: var(--wire-color-text);
|
||||
}
|
||||
.wire-typography :where(code) {
|
||||
border-radius: 0.35rem;
|
||||
background: var(--wire-color-surface-soft);
|
||||
padding: 0.15rem 0.35rem;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.wire-typography :where(pre) {
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--wire-color-border);
|
||||
border-radius: 1rem;
|
||||
background: var(--wire-color-surface-raised);
|
||||
padding: 1rem;
|
||||
}
|
||||
.wire-typography :where(img) { border-radius: 1rem; }
|
||||
.wire-typography :where(hr) { border-color: var(--wire-color-border); margin: 2rem 0; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
component WysiwygEditor {
|
||||
outputs {
|
||||
input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||
}
|
||||
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Wysiwyg Editor"
|
||||
description = ""
|
||||
items = []
|
||||
variant = "default"
|
||||
class = ""
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
title: string = "Wysiwyg Editor"
|
||||
description: string = ""
|
||||
items: unknown[] = []
|
||||
variant: string = "default"
|
||||
class: string = ""
|
||||
}
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--wysiwyg-editor wire-next--variant-{variant} {class}">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user