@a4ui/core 0.13.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -38
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1721 -1598
- package/dist/ui/Ripple.d.ts +25 -0
- package/dist/ui/Typewriter.d.ts +28 -0
- package/package.json +14 -6
- package/src/index.ts +3 -1
- package/src/ui/Ripple.tsx +119 -0
- package/src/ui/Typewriter.tsx +149 -0
- package/src/elements.tsx +0 -167
- package/src/styles/space.css +0 -510
- package/src/styles/tokens.css +0 -213
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { JSX } from 'solid-js';
|
|
2
|
+
export interface RippleProps {
|
|
3
|
+
children: JSX.Element;
|
|
4
|
+
/** Ripple color (CSS color). @default 'currentColor' */
|
|
5
|
+
color?: string;
|
|
6
|
+
/** Ripple opacity 0..1. @default 0.3 */
|
|
7
|
+
opacity?: number;
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Wraps `children` in a `position: relative; overflow: hidden` layer that
|
|
12
|
+
* spawns a Material-style ripple — a circle centered on the pointer's down
|
|
13
|
+
* position, sized to cover the wrapped content, expanding and fading out —
|
|
14
|
+
* on every `pointerdown`. Rapid clicks stack multiple ripples; each removes
|
|
15
|
+
* itself once its animation finishes. A no-op under reduced motion: children
|
|
16
|
+
* still render, but no ripple ever spawns.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* <Ripple color="white" opacity={0.25}>
|
|
21
|
+
* <button class="rounded-lg bg-primary px-4 py-2 text-primary-foreground">Save</button>
|
|
22
|
+
* </Ripple>
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function Ripple(props: RippleProps): JSX.Element;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { JSX } from 'solid-js';
|
|
2
|
+
export interface TypewriterProps {
|
|
3
|
+
/** The full string (or strings, cycled) to type out. */
|
|
4
|
+
text: string | string[];
|
|
5
|
+
/** ms per character. @default 55 */
|
|
6
|
+
speed?: number;
|
|
7
|
+
/** ms to pause on a completed string before deleting (only when `text` is an array). @default 1400 */
|
|
8
|
+
pause?: number;
|
|
9
|
+
/** Show a blinking caret. @default true */
|
|
10
|
+
caret?: boolean;
|
|
11
|
+
/** Loop through the array forever. @default true when text is an array */
|
|
12
|
+
loop?: boolean;
|
|
13
|
+
class?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Types `text` out one character at a time, like a terminal. When `text` is
|
|
17
|
+
* an array, each string types in, holds for `pause` ms, then deletes before
|
|
18
|
+
* the next one types in — cycling forever unless `loop` is `false` (defaults
|
|
19
|
+
* to looping only when there's more than one string). Shows the first (or
|
|
20
|
+
* only) string statically, with no typing and no caret blink, when the user
|
|
21
|
+
* prefers reduced motion.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* <Typewriter text={['Building things…', 'Shipping things…']} speed={40} />
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare function Typewriter(props: TypewriterProps): JSX.Element;
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a4ui/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "A4ui — Spatial Glass design system & component library for SolidJS (Kobalte behavior + Tailwind glass tokens + motion).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Luis Alfredo Rivera Acuña <1alfredorivera@gmail.com>",
|
|
8
|
-
"homepage": "https://
|
|
8
|
+
"homepage": "https://a4ui.pages.dev/",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "git+https://github.com/A4uikit/a4ui.git"
|
|
@@ -29,10 +29,16 @@
|
|
|
29
29
|
],
|
|
30
30
|
"files": [
|
|
31
31
|
"dist",
|
|
32
|
-
"
|
|
32
|
+
"preset.js",
|
|
33
|
+
"src/index.ts",
|
|
34
|
+
"src/ui",
|
|
35
|
+
"src/lib",
|
|
36
|
+
"src/layout",
|
|
37
|
+
"src/themes",
|
|
38
|
+
"src/commerce",
|
|
39
|
+
"src/charts",
|
|
33
40
|
"!src/**/*.test.ts",
|
|
34
|
-
"!src/**/*.test.tsx"
|
|
35
|
-
"preset.js"
|
|
41
|
+
"!src/**/*.test.tsx"
|
|
36
42
|
],
|
|
37
43
|
"main": "./dist/index.js",
|
|
38
44
|
"module": "./dist/index.js",
|
|
@@ -72,10 +78,12 @@
|
|
|
72
78
|
"engines": {
|
|
73
79
|
"node": ">=20"
|
|
74
80
|
},
|
|
81
|
+
"packageManager": "npm@10.9.3",
|
|
75
82
|
"publishConfig": {
|
|
76
83
|
"access": "public"
|
|
77
84
|
},
|
|
78
85
|
"scripts": {
|
|
86
|
+
"prebuild": "node scripts/sync-version.mjs",
|
|
79
87
|
"build": "vite build && npm run build:elements",
|
|
80
88
|
"build:elements": "vite build --config vite.elements.config.ts && node scripts/build-elements-css.mjs",
|
|
81
89
|
"dev": "vite build --watch",
|
|
@@ -92,7 +100,7 @@
|
|
|
92
100
|
"test:a11y": "playwright test a11y --project=desktop",
|
|
93
101
|
"test:report": "playwright show-report",
|
|
94
102
|
"test:package": "node scripts/test-package.mjs",
|
|
95
|
-
"validate": "npm run typecheck && npm run lint && npm run test:unit && npm run build",
|
|
103
|
+
"validate": "npm run typecheck && npm run lint && npm run format:check && npm run test:unit && npm run build && npm run test:package",
|
|
96
104
|
"prepublishOnly": "npm run validate",
|
|
97
105
|
"prepare": "husky"
|
|
98
106
|
},
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// import '@a4ui/core/styles.css'
|
|
9
9
|
// import { Button, Card, Modal } from '@a4ui/core'
|
|
10
10
|
|
|
11
|
-
export const A4UI_VERSION = '0.
|
|
11
|
+
export const A4UI_VERSION = '0.14.0'
|
|
12
12
|
|
|
13
13
|
// Helpers (src/lib) — generic, framework-level utilities.
|
|
14
14
|
export { cn } from './lib/cn'
|
|
@@ -149,6 +149,8 @@ export {
|
|
|
149
149
|
export { MultiStateBadge, type MultiStateBadgeProps, type BadgeState } from './ui/MultiStateBadge'
|
|
150
150
|
export { NowPlaying, type NowPlayingProps } from './ui/NowPlaying'
|
|
151
151
|
export { Expandable, type ExpandableProps } from './ui/Expandable'
|
|
152
|
+
export { Typewriter, type TypewriterProps } from './ui/Typewriter'
|
|
153
|
+
export { Ripple, type RippleProps } from './ui/Ripple'
|
|
152
154
|
export { flyToCart, type FlyToCartOptions } from './lib/flyToCart'
|
|
153
155
|
|
|
154
156
|
// Layout (src/layout) — generic shell + backdrop + toggles. App-coupled pieces
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Material-style click ripple: a wrapper that spawns an expanding, fading
|
|
2
|
+
// circle from the pointer's down position on every `pointerdown` — Motion's
|
|
3
|
+
// `animate` drives the scale/opacity tween since each ripple is a one-shot
|
|
4
|
+
// spawn-then-discard element, not a persistent CSS transition. `x`/`y` are
|
|
5
|
+
// animated to a constant -50% alongside `scale` so Motion (which owns the
|
|
6
|
+
// element's `transform` once it's animating any transform sub-property)
|
|
7
|
+
// composes `translate(-50%,-50%) scale(...)` itself rather than clobbering a
|
|
8
|
+
// translate we'd otherwise have set by hand via inline style.
|
|
9
|
+
import { createSignal, For, onCleanup, type JSX } from 'solid-js'
|
|
10
|
+
|
|
11
|
+
import { cn } from '../lib/cn'
|
|
12
|
+
import { animate, motionReduced } from '../lib/motion'
|
|
13
|
+
|
|
14
|
+
export interface RippleProps {
|
|
15
|
+
children: JSX.Element
|
|
16
|
+
/** Ripple color (CSS color). @default 'currentColor' */
|
|
17
|
+
color?: string
|
|
18
|
+
/** Ripple opacity 0..1. @default 0.3 */
|
|
19
|
+
opacity?: number
|
|
20
|
+
class?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface RippleInstance {
|
|
24
|
+
id: number
|
|
25
|
+
x: number
|
|
26
|
+
y: number
|
|
27
|
+
size: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let nextRippleId = 0
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Wraps `children` in a `position: relative; overflow: hidden` layer that
|
|
34
|
+
* spawns a Material-style ripple — a circle centered on the pointer's down
|
|
35
|
+
* position, sized to cover the wrapped content, expanding and fading out —
|
|
36
|
+
* on every `pointerdown`. Rapid clicks stack multiple ripples; each removes
|
|
37
|
+
* itself once its animation finishes. A no-op under reduced motion: children
|
|
38
|
+
* still render, but no ripple ever spawns.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```tsx
|
|
42
|
+
* <Ripple color="white" opacity={0.25}>
|
|
43
|
+
* <button class="rounded-lg bg-primary px-4 py-2 text-primary-foreground">Save</button>
|
|
44
|
+
* </Ripple>
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function Ripple(props: RippleProps): JSX.Element {
|
|
48
|
+
const [ripples, setRipples] = createSignal<RippleInstance[]>([])
|
|
49
|
+
const active = new Set<ReturnType<typeof animate>>()
|
|
50
|
+
|
|
51
|
+
let containerEl: HTMLSpanElement | undefined
|
|
52
|
+
|
|
53
|
+
const remove = (id: number): void => {
|
|
54
|
+
setRipples((current) => current.filter((ripple) => ripple.id !== id))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const spawnAnimation = (el: HTMLSpanElement, id: number, opacity: number): void => {
|
|
58
|
+
const controls = animate(
|
|
59
|
+
el,
|
|
60
|
+
{ x: ['-50%', '-50%'], y: ['-50%', '-50%'], scale: [0, 1], opacity: [opacity, 0] },
|
|
61
|
+
{ duration: 0.6, ease: 'easeOut' },
|
|
62
|
+
)
|
|
63
|
+
active.add(controls)
|
|
64
|
+
controls.finished
|
|
65
|
+
.then(() => {
|
|
66
|
+
active.delete(controls)
|
|
67
|
+
remove(id)
|
|
68
|
+
})
|
|
69
|
+
.catch(() => {})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const handlePointerDown = (event: PointerEvent): void => {
|
|
73
|
+
if (motionReduced() || !containerEl) return
|
|
74
|
+
|
|
75
|
+
const rect = containerEl.getBoundingClientRect()
|
|
76
|
+
const x = event.clientX - rect.left
|
|
77
|
+
const y = event.clientY - rect.top
|
|
78
|
+
const maxDist = Math.max(
|
|
79
|
+
Math.hypot(x, y),
|
|
80
|
+
Math.hypot(rect.width - x, y),
|
|
81
|
+
Math.hypot(x, rect.height - y),
|
|
82
|
+
Math.hypot(rect.width - x, rect.height - y),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
setRipples((current) => [...current, { id: nextRippleId++, x, y, size: maxDist * 2 }])
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
onCleanup(() => {
|
|
89
|
+
active.forEach((controls) => controls.stop())
|
|
90
|
+
active.clear()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<span
|
|
95
|
+
ref={containerEl}
|
|
96
|
+
class={cn('relative inline-block overflow-hidden', props.class)}
|
|
97
|
+
onPointerDown={handlePointerDown}
|
|
98
|
+
>
|
|
99
|
+
{props.children}
|
|
100
|
+
<span aria-hidden="true" class="pointer-events-none absolute inset-0">
|
|
101
|
+
<For each={ripples()}>
|
|
102
|
+
{(ripple) => (
|
|
103
|
+
<span
|
|
104
|
+
ref={(el) => spawnAnimation(el, ripple.id, props.opacity ?? 0.3)}
|
|
105
|
+
class="absolute rounded-full"
|
|
106
|
+
style={{
|
|
107
|
+
left: `${ripple.x}px`,
|
|
108
|
+
top: `${ripple.y}px`,
|
|
109
|
+
width: `${ripple.size}px`,
|
|
110
|
+
height: `${ripple.size}px`,
|
|
111
|
+
background: props.color ?? 'currentColor',
|
|
112
|
+
}}
|
|
113
|
+
/>
|
|
114
|
+
)}
|
|
115
|
+
</For>
|
|
116
|
+
</span>
|
|
117
|
+
</span>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Terminal-style text reveal: characters appear one at a time via a plain
|
|
2
|
+
// setTimeout loop (not a CSS transition, since each character is a discrete
|
|
3
|
+
// reveal step, not a tween). Accepts a single string or an array to cycle
|
|
4
|
+
// through — type, pause, delete, type the next — with an optional blinking
|
|
5
|
+
// caret, whose keyframe is injected once into the document head (mirroring
|
|
6
|
+
// LoadingDots's approach to a component-scoped CSS animation).
|
|
7
|
+
import { createSignal, onCleanup, onMount, type JSX } from 'solid-js'
|
|
8
|
+
|
|
9
|
+
import { cn } from '../lib/cn'
|
|
10
|
+
import { motionReduced } from '../lib/motion'
|
|
11
|
+
|
|
12
|
+
export interface TypewriterProps {
|
|
13
|
+
/** The full string (or strings, cycled) to type out. */
|
|
14
|
+
text: string | string[]
|
|
15
|
+
/** ms per character. @default 55 */
|
|
16
|
+
speed?: number
|
|
17
|
+
/** ms to pause on a completed string before deleting (only when `text` is an array). @default 1400 */
|
|
18
|
+
pause?: number
|
|
19
|
+
/** Show a blinking caret. @default true */
|
|
20
|
+
caret?: boolean
|
|
21
|
+
/** Loop through the array forever. @default true when text is an array */
|
|
22
|
+
loop?: boolean
|
|
23
|
+
class?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const STYLE_ID = 'a4ui-typewriter-style'
|
|
27
|
+
|
|
28
|
+
function ensureStyleInjected(): void {
|
|
29
|
+
if (typeof document === 'undefined' || document.getElementById(STYLE_ID)) return
|
|
30
|
+
const style = document.createElement('style')
|
|
31
|
+
style.id = STYLE_ID
|
|
32
|
+
style.textContent = `
|
|
33
|
+
@keyframes a4-caret-blink {
|
|
34
|
+
0%, 49% { opacity: 1; }
|
|
35
|
+
50%, 100% { opacity: 0; }
|
|
36
|
+
}
|
|
37
|
+
`
|
|
38
|
+
document.head.appendChild(style)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function toList(text: string | string[]): string[] {
|
|
42
|
+
return Array.isArray(text) ? text : [text]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Types `text` out one character at a time, like a terminal. When `text` is
|
|
47
|
+
* an array, each string types in, holds for `pause` ms, then deletes before
|
|
48
|
+
* the next one types in — cycling forever unless `loop` is `false` (defaults
|
|
49
|
+
* to looping only when there's more than one string). Shows the first (or
|
|
50
|
+
* only) string statically, with no typing and no caret blink, when the user
|
|
51
|
+
* prefers reduced motion.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```tsx
|
|
55
|
+
* <Typewriter text={['Building things…', 'Shipping things…']} speed={40} />
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export function Typewriter(props: TypewriterProps): JSX.Element {
|
|
59
|
+
ensureStyleInjected()
|
|
60
|
+
|
|
61
|
+
// eslint-disable-next-line solid/reactivity -- one-time seed; run() re-reads props.text on each (re)start
|
|
62
|
+
const [display, setDisplay] = createSignal(motionReduced() ? (toList(props.text)[0] ?? '') : '')
|
|
63
|
+
|
|
64
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
65
|
+
|
|
66
|
+
const stop = (): void => {
|
|
67
|
+
if (timer === undefined) return
|
|
68
|
+
clearTimeout(timer)
|
|
69
|
+
timer = undefined
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const run = (): void => {
|
|
73
|
+
stop()
|
|
74
|
+
const list = toList(props.text)
|
|
75
|
+
const first = list[0] ?? ''
|
|
76
|
+
|
|
77
|
+
if (motionReduced() || list.length === 0) {
|
|
78
|
+
setDisplay(first)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const speed = props.speed ?? 55
|
|
83
|
+
const pause = props.pause ?? 1400
|
|
84
|
+
const shouldLoop = props.loop ?? list.length > 1
|
|
85
|
+
|
|
86
|
+
let stringIndex = 0
|
|
87
|
+
let charIndex = 0
|
|
88
|
+
let phase: 'typing' | 'pausing' | 'deleting' = 'typing'
|
|
89
|
+
|
|
90
|
+
const step = (): void => {
|
|
91
|
+
const text = list[stringIndex] ?? ''
|
|
92
|
+
|
|
93
|
+
if (phase === 'typing') {
|
|
94
|
+
charIndex += 1
|
|
95
|
+
setDisplay(text.slice(0, charIndex))
|
|
96
|
+
if (charIndex >= text.length) {
|
|
97
|
+
const isLastString = stringIndex >= list.length - 1
|
|
98
|
+
if (list.length <= 1 || (isLastString && !shouldLoop)) {
|
|
99
|
+
stop()
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
phase = 'pausing'
|
|
103
|
+
timer = setTimeout(step, pause)
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
timer = setTimeout(step, speed)
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (phase === 'pausing') {
|
|
111
|
+
phase = 'deleting'
|
|
112
|
+
timer = setTimeout(step, speed)
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// deleting
|
|
117
|
+
charIndex -= 1
|
|
118
|
+
setDisplay(text.slice(0, Math.max(0, charIndex)))
|
|
119
|
+
if (charIndex <= 0) {
|
|
120
|
+
stringIndex = (stringIndex + 1) % list.length
|
|
121
|
+
phase = 'typing'
|
|
122
|
+
}
|
|
123
|
+
timer = setTimeout(step, speed)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
setDisplay('')
|
|
127
|
+
timer = setTimeout(step, speed)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
onMount(run)
|
|
131
|
+
onCleanup(stop)
|
|
132
|
+
|
|
133
|
+
const showCaret = (): boolean => (props.caret ?? true) && !motionReduced()
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<span class={cn('font-mono', props.class)}>
|
|
137
|
+
{display()}
|
|
138
|
+
{showCaret() && (
|
|
139
|
+
<span
|
|
140
|
+
aria-hidden="true"
|
|
141
|
+
class="inline-block"
|
|
142
|
+
style={{ animation: 'a4-caret-blink 1s step-end infinite' }}
|
|
143
|
+
>
|
|
144
|
+
|
|
|
145
|
+
</span>
|
|
146
|
+
)}
|
|
147
|
+
</span>
|
|
148
|
+
)
|
|
149
|
+
}
|
package/src/elements.tsx
DELETED
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
// Web Components entry (`@a4ui/core/elements`) — a curated subset of A4ui's
|
|
2
|
-
// presentational components registered as framework-agnostic custom elements so
|
|
3
|
-
// they can be used from React/Next.js, Vue, Svelte, or plain HTML.
|
|
4
|
-
//
|
|
5
|
-
// Design notes:
|
|
6
|
-
// - Each element renders in LIGHT DOM (noShadowDOM) so the global stylesheet
|
|
7
|
-
// (`@a4ui/core/elements.css`) and CSS theme variables apply normally — Shadow
|
|
8
|
-
// DOM would isolate the Tailwind utility classes the components rely on.
|
|
9
|
-
// - Props map to attributes/properties (solid-element coerces by the declared
|
|
10
|
-
// default's type: number/boolean/string). Text content is taken from a `label`
|
|
11
|
-
// or `text` attribute, falling back to the element's initial text.
|
|
12
|
-
// - Only leaf, primitive-prop components are wrapped. Rich/interactive/portalled
|
|
13
|
-
// components (Modal, Combobox, DataGrid, …) remain SolidJS-only — see the docs.
|
|
14
|
-
//
|
|
15
|
-
// This bundle is self-contained (solid-js is bundled in), so non-Solid apps need
|
|
16
|
-
// no Solid toolchain: `import '@a4ui/core/elements'` once, then use the tags.
|
|
17
|
-
import { customElement, noShadowDOM } from 'solid-element'
|
|
18
|
-
import type { JSX } from 'solid-js'
|
|
19
|
-
|
|
20
|
-
import { Alert, type AlertTone } from './ui/Alert'
|
|
21
|
-
import { Avatar } from './ui/Avatar'
|
|
22
|
-
import { Badge, type BadgeTone } from './ui/Badge'
|
|
23
|
-
import { Button, type ButtonVariant } from './ui/Button'
|
|
24
|
-
import { Clock } from './ui/Clock'
|
|
25
|
-
import { Countdown } from './ui/Countdown'
|
|
26
|
-
import { Kbd } from './ui/Kbd'
|
|
27
|
-
import { Meter } from './ui/Meter'
|
|
28
|
-
import { Progress } from './ui/Progress'
|
|
29
|
-
import { Rating } from './ui/Rating'
|
|
30
|
-
import { RingProgress } from './ui/RingProgress'
|
|
31
|
-
import { Separator } from './ui/Separator'
|
|
32
|
-
import { Spinner } from './ui/Spinner'
|
|
33
|
-
import { Stat, type StatTone } from './ui/Stat'
|
|
34
|
-
|
|
35
|
-
/** The initial text an author put between the tags, before Solid renders over it. */
|
|
36
|
-
function initialText(element: unknown): string {
|
|
37
|
-
return ((element as HTMLElement).textContent ?? '').trim()
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Register every A4ui custom element. Import for its side effect. */
|
|
41
|
-
export function defineA4uiElements(): void {
|
|
42
|
-
customElement(
|
|
43
|
-
'a4-button',
|
|
44
|
-
{ variant: 'primary', disabled: false, type: 'button', label: '' },
|
|
45
|
-
(props, { element }): JSX.Element => {
|
|
46
|
-
noShadowDOM()
|
|
47
|
-
const text = props.label || initialText(element)
|
|
48
|
-
return (
|
|
49
|
-
<Button
|
|
50
|
-
variant={props.variant as ButtonVariant}
|
|
51
|
-
disabled={props.disabled}
|
|
52
|
-
type={props.type as 'button' | 'submit' | 'reset'}
|
|
53
|
-
>
|
|
54
|
-
{text}
|
|
55
|
-
</Button>
|
|
56
|
-
)
|
|
57
|
-
},
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
customElement('a4-badge', { tone: 'neutral', label: '' }, (props, { element }): JSX.Element => {
|
|
61
|
-
noShadowDOM()
|
|
62
|
-
return <Badge tone={props.tone as BadgeTone}>{props.label || initialText(element)}</Badge>
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
customElement(
|
|
66
|
-
// `heading`, not `title` — the latter is a global HTML attribute and would
|
|
67
|
-
// clash with the element's native `title` (tooltip) property.
|
|
68
|
-
'a4-alert',
|
|
69
|
-
{ tone: 'info', heading: '', text: '' },
|
|
70
|
-
(props, { element }): JSX.Element => {
|
|
71
|
-
noShadowDOM()
|
|
72
|
-
return (
|
|
73
|
-
<Alert tone={props.tone as AlertTone} title={props.heading || undefined}>
|
|
74
|
-
{props.text || initialText(element)}
|
|
75
|
-
</Alert>
|
|
76
|
-
)
|
|
77
|
-
},
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
customElement('a4-spinner', { label: 'Loading' }, (props): JSX.Element => {
|
|
81
|
-
noShadowDOM()
|
|
82
|
-
return <Spinner label={props.label} />
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
customElement('a4-avatar', { src: '', alt: '', fallback: '' }, (props): JSX.Element => {
|
|
86
|
-
noShadowDOM()
|
|
87
|
-
return <Avatar src={props.src || undefined} alt={props.alt} fallback={props.fallback || ''} />
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
customElement('a4-progress', { value: 0, max: 100, label: '' }, (props): JSX.Element => {
|
|
91
|
-
noShadowDOM()
|
|
92
|
-
return <Progress value={props.value} max={props.max} label={props.label || undefined} />
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
customElement('a4-meter', { value: 0, max: 100, label: '' }, (props): JSX.Element => {
|
|
96
|
-
noShadowDOM()
|
|
97
|
-
return <Meter value={props.value} max={props.max} label={props.label || undefined} />
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
customElement('a4-ring-progress', { value: 0, size: 96, thickness: 8, label: '' }, (props): JSX.Element => {
|
|
101
|
-
noShadowDOM()
|
|
102
|
-
return (
|
|
103
|
-
<RingProgress
|
|
104
|
-
value={props.value}
|
|
105
|
-
size={props.size}
|
|
106
|
-
thickness={props.thickness}
|
|
107
|
-
label={props.label || undefined}
|
|
108
|
-
/>
|
|
109
|
-
)
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
customElement('a4-stat', { label: '', value: 0, tone: 'neutral' }, (props): JSX.Element => {
|
|
113
|
-
noShadowDOM()
|
|
114
|
-
return <Stat label={props.label} value={props.value} tone={props.tone as StatTone} />
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
customElement('a4-kbd', { label: '' }, (props, { element }): JSX.Element => {
|
|
118
|
-
noShadowDOM()
|
|
119
|
-
return <Kbd>{props.label || initialText(element)}</Kbd>
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
customElement('a4-separator', { orientation: 'horizontal' }, (props): JSX.Element => {
|
|
123
|
-
noShadowDOM()
|
|
124
|
-
return <Separator orientation={props.orientation as 'horizontal' | 'vertical'} />
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
customElement('a4-rating', { value: 0, max: 5, readonly: false }, (props, { element }): JSX.Element => {
|
|
128
|
-
noShadowDOM()
|
|
129
|
-
return (
|
|
130
|
-
<Rating
|
|
131
|
-
value={props.value}
|
|
132
|
-
max={props.max}
|
|
133
|
-
readonly={props.readonly}
|
|
134
|
-
onChange={(v) =>
|
|
135
|
-
(element as unknown as HTMLElement).dispatchEvent(new CustomEvent('change', { detail: v }))
|
|
136
|
-
}
|
|
137
|
-
/>
|
|
138
|
-
)
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
customElement('a4-countdown', { to: '' }, (props): JSX.Element => {
|
|
142
|
-
noShadowDOM()
|
|
143
|
-
// eslint-disable-next-line solid/reactivity -- read the `to` attribute once to fix the target
|
|
144
|
-
const target = props.to ? new Date(props.to) : new Date(Date.now() + 86_400_000)
|
|
145
|
-
return <Countdown to={target} />
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
customElement(
|
|
149
|
-
'a4-clock',
|
|
150
|
-
{ variant: 'digital', seconds: true, hour12: false, timezone: '', size: 160 },
|
|
151
|
-
(props): JSX.Element => {
|
|
152
|
-
noShadowDOM()
|
|
153
|
-
return (
|
|
154
|
-
<Clock
|
|
155
|
-
variant={props.variant as 'digital' | 'analog'}
|
|
156
|
-
seconds={props.seconds}
|
|
157
|
-
hour12={props.hour12}
|
|
158
|
-
timeZone={props.timezone || undefined}
|
|
159
|
-
size={props.size}
|
|
160
|
-
/>
|
|
161
|
-
)
|
|
162
|
-
},
|
|
163
|
-
)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Registering on import is the ergonomic default for `import '@a4ui/core/elements'`.
|
|
167
|
-
defineA4uiElements()
|