@proyecto-viviana/ui 0.3.2 → 0.3.4
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/dist/components.css +1077 -1077
- package/dist/index.js +236 -249
- package/dist/index.js.map +3 -3
- package/dist/index.ssr.js +78 -81
- package/dist/index.ssr.js.map +3 -3
- package/dist/radio/index.d.ts +12 -27
- package/dist/radio/index.d.ts.map +1 -1
- package/dist/test-utils/index.d.ts +2 -2
- package/dist/test-utils/index.d.ts.map +1 -1
- package/package.json +13 -12
- package/src/alert/index.tsx +48 -0
- package/src/assets/favicon.png +0 -0
- package/src/assets/fire.gif +0 -0
- package/src/autocomplete/index.tsx +313 -0
- package/src/avatar/index.tsx +75 -0
- package/src/badge/index.tsx +43 -0
- package/src/breadcrumbs/index.tsx +207 -0
- package/src/button/Button.tsx +74 -0
- package/src/button/index.ts +2 -0
- package/src/button/types.ts +24 -0
- package/src/calendar/DateField.tsx +200 -0
- package/src/calendar/DatePicker.tsx +298 -0
- package/src/calendar/RangeCalendar.tsx +236 -0
- package/src/calendar/TimeField.tsx +196 -0
- package/src/calendar/index.tsx +223 -0
- package/src/checkbox/index.tsx +257 -0
- package/src/color/index.tsx +687 -0
- package/src/combobox/index.tsx +383 -0
- package/src/components.css +1077 -0
- package/src/custom/calendar-card/index.tsx +66 -0
- package/src/custom/chip/index.tsx +46 -0
- package/src/custom/conversation/index.tsx +105 -0
- package/src/custom/event-card/index.tsx +132 -0
- package/src/custom/header/index.tsx +33 -0
- package/src/custom/lateral-nav/index.tsx +88 -0
- package/src/custom/logo/index.tsx +58 -0
- package/src/custom/nav-header/index.tsx +42 -0
- package/src/custom/page-layout/index.tsx +29 -0
- package/src/custom/profile-card/index.tsx +64 -0
- package/src/custom/project-card/index.tsx +59 -0
- package/src/custom/timeline-item/index.tsx +105 -0
- package/src/dialog/Dialog.tsx +260 -0
- package/src/dialog/index.tsx +3 -0
- package/src/disclosure/index.tsx +307 -0
- package/src/gridlist/index.tsx +403 -0
- package/src/icon/icons/GitHubIcon.tsx +20 -0
- package/src/icon/index.tsx +48 -0
- package/src/index.ts +322 -0
- package/src/landmark/index.tsx +231 -0
- package/src/link/index.tsx +130 -0
- package/src/listbox/index.tsx +231 -0
- package/src/menu/index.tsx +297 -0
- package/src/meter/index.tsx +163 -0
- package/src/numberfield/index.tsx +482 -0
- package/src/popover/index.tsx +260 -0
- package/src/progress-bar/index.tsx +169 -0
- package/src/radio/index.tsx +173 -0
- package/src/searchfield/index.tsx +453 -0
- package/src/select/index.tsx +349 -0
- package/src/separator/index.tsx +141 -0
- package/src/slider/index.tsx +382 -0
- package/src/styles.css +450 -0
- package/src/switch/ToggleSwitch.tsx +112 -0
- package/src/switch/index.tsx +90 -0
- package/src/table/index.tsx +531 -0
- package/src/tabs/index.tsx +273 -0
- package/src/tag-group/index.tsx +240 -0
- package/src/test-utils/index.ts +40 -0
- package/src/textfield/index.tsx +211 -0
- package/src/theme.css +101 -0
- package/src/toast/index.tsx +324 -0
- package/src/toolbar/index.tsx +108 -0
- package/src/tooltip/index.tsx +197 -0
- package/src/tree/index.tsx +494 -0
- package/dist/index.jsx +0 -6658
- package/dist/index.jsx.map +0 -7
package/dist/radio/index.d.ts
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* SSR-compatible - renders children and UI elements directly without render props.
|
|
6
6
|
*/
|
|
7
7
|
import { type JSX } from 'solid-js';
|
|
8
|
-
import { type RadioProps as HeadlessRadioProps } from '@proyecto-viviana/solidaria-components';
|
|
8
|
+
import { type RadioGroupProps as HeadlessRadioGroupProps, type RadioProps as HeadlessRadioProps } from '@proyecto-viviana/solidaria-components';
|
|
9
9
|
export type RadioGroupOrientation = 'horizontal' | 'vertical';
|
|
10
10
|
export type RadioGroupSize = 'sm' | 'md' | 'lg';
|
|
11
|
-
export
|
|
11
|
+
export interface RadioGroupProps extends Omit<HeadlessRadioGroupProps, 'class' | 'style'> {
|
|
12
12
|
/** The size of the radio buttons. */
|
|
13
13
|
size?: RadioGroupSize;
|
|
14
14
|
/** Additional CSS class name. */
|
|
@@ -19,32 +19,10 @@ export type RadioGroupProps = {
|
|
|
19
19
|
description?: string;
|
|
20
20
|
/** Error message when invalid. */
|
|
21
21
|
errorMessage?: string;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
/** The default value (uncontrolled). */
|
|
25
|
-
defaultValue?: string;
|
|
26
|
-
/** Callback when value changes. */
|
|
27
|
-
onChange?: (value: string) => void;
|
|
28
|
-
/** The orientation of the radio group. */
|
|
29
|
-
orientation?: 'horizontal' | 'vertical';
|
|
30
|
-
/** Whether the radio group is disabled. */
|
|
31
|
-
isDisabled?: boolean;
|
|
32
|
-
/** Whether the radio group is read only. */
|
|
33
|
-
isReadOnly?: boolean;
|
|
34
|
-
/** Whether the radio group is required. */
|
|
35
|
-
isRequired?: boolean;
|
|
36
|
-
/** Whether the radio group is invalid. */
|
|
37
|
-
isInvalid?: boolean;
|
|
38
|
-
/** The children of the component. */
|
|
39
|
-
children?: JSX.Element;
|
|
40
|
-
/** The name of the radio group (for form submission). */
|
|
41
|
-
name?: string;
|
|
42
|
-
};
|
|
43
|
-
export interface RadioProps extends Omit<HeadlessRadioProps, 'class' | 'style' | 'children'> {
|
|
22
|
+
}
|
|
23
|
+
export interface RadioProps extends Omit<HeadlessRadioProps, 'class' | 'style'> {
|
|
44
24
|
/** Additional CSS class name. */
|
|
45
25
|
class?: string;
|
|
46
|
-
/** The content/label for the radio button. */
|
|
47
|
-
children?: JSX.Element;
|
|
48
26
|
}
|
|
49
27
|
/**
|
|
50
28
|
* A radio group allows users to select a single option from a list of mutually exclusive options.
|
|
@@ -53,7 +31,14 @@ export interface RadioProps extends Omit<HeadlessRadioProps, 'class' | 'style' |
|
|
|
53
31
|
*/
|
|
54
32
|
export declare function RadioGroup(props: RadioGroupProps): JSX.Element;
|
|
55
33
|
/**
|
|
56
|
-
* A radio button allows
|
|
34
|
+
* A radio button allows users to select a single option from a list.
|
|
35
|
+
* Must be used within a RadioGroup.
|
|
36
|
+
* SSR-compatible - renders static JSX without render prop children.
|
|
37
|
+
*
|
|
38
|
+
* Note: Unlike other styled components, Radio does not use render props for children.
|
|
39
|
+
* Instead, it relies on data attributes set by the headless Radio component for styling.
|
|
40
|
+
* However, since we need dynamic styling based on state, we accept that this component
|
|
41
|
+
* has some limitations compared to the render-props-based original implementation.
|
|
57
42
|
*
|
|
58
43
|
* Built on solidaria-components Radio for full accessibility support.
|
|
59
44
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/radio/index.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,KAAK,GAAG,EAA+C,MAAM,UAAU,CAAA;AAChF,OAAO,EAGL,KAAK,UAAU,IAAI,kBAAkB,EAGtC,MAAM,wCAAwC,CAAA;AAM/C,MAAM,MAAM,qBAAqB,GAAG,YAAY,GAAG,UAAU,CAAA;AAC7D,MAAM,MAAM,cAAc,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;AAQ/C,MAAM,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/radio/index.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,KAAK,GAAG,EAA+C,MAAM,UAAU,CAAA;AAChF,OAAO,EAGL,KAAK,eAAe,IAAI,uBAAuB,EAC/C,KAAK,UAAU,IAAI,kBAAkB,EAGtC,MAAM,wCAAwC,CAAA;AAM/C,MAAM,MAAM,qBAAqB,GAAG,YAAY,GAAG,UAAU,CAAA;AAC7D,MAAM,MAAM,cAAc,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;AAQ/C,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAAC,uBAAuB,EAAE,OAAO,GAAG,OAAO,CAAC;IACvF,qCAAqC;IACrC,IAAI,CAAC,EAAE,cAAc,CAAA;IACrB,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,2BAA2B;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,iCAAiC;IACjC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,kCAAkC;IAClC,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,UAAW,SAAQ,IAAI,CAAC,kBAAkB,EAAE,OAAO,GAAG,OAAO,CAAC;IAC7E,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AA4BD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,GAAG,CAAC,OAAO,CA2C9D;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,GAAG,CAAC,OAAO,CAgCpD"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import userEvent from '@testing-library/user-event';
|
|
1
|
+
import userEvent, { type UserEvent } from '@testing-library/user-event';
|
|
2
2
|
/**
|
|
3
3
|
* Pointer map matching react-spectrum's test setup.
|
|
4
4
|
* Ensures pointer events have realistic dimensions so they aren't mistaken for virtual clicks.
|
|
@@ -38,7 +38,7 @@ export declare const pointerMap: ({
|
|
|
38
38
|
* - pointerMap - Realistic pointer event dimensions
|
|
39
39
|
* - pointerEventsCheck: Never - Skip pointer-events CSS check (jsdom doesn't handle this well)
|
|
40
40
|
*/
|
|
41
|
-
export declare function setupUser():
|
|
41
|
+
export declare function setupUser(): UserEvent;
|
|
42
42
|
export { render, screen, fireEvent, cleanup } from '@solidjs/testing-library';
|
|
43
43
|
export { userEvent };
|
|
44
44
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/test-utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/test-utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,EAAE,EAAE,KAAK,SAAS,EAA2B,MAAM,6BAA6B,CAAC;AAEjG;;;GAGG;AACH,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAOtB,CAAC;AASF;;;;;GAKG;AACH,wBAAgB,SAAS,IAAI,SAAS,CAQrC;AAED,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAC9E,OAAO,EAAE,SAAS,EAAE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,45 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proyecto-viviana/ui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Styled UI components for SolidJS - inspired by React Spectrum",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "./dist/index.
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
7
|
"module": "./dist/index.js",
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
|
-
"solid": "./
|
|
12
|
+
"solid": "./src/index.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
13
14
|
"default": "./dist/index.js"
|
|
14
15
|
},
|
|
15
16
|
"./theme.css": {
|
|
16
17
|
"import": "./dist/theme.css",
|
|
17
|
-
"default": "./
|
|
18
|
+
"default": "./src/theme.css"
|
|
18
19
|
},
|
|
19
20
|
"./styles.css": {
|
|
20
21
|
"import": "./dist/styles.css",
|
|
21
|
-
"default": "./
|
|
22
|
+
"default": "./src/styles.css"
|
|
22
23
|
},
|
|
23
24
|
"./components.css": {
|
|
24
25
|
"import": "./dist/components.css",
|
|
25
|
-
"default": "./
|
|
26
|
+
"default": "./src/components.css"
|
|
26
27
|
}
|
|
27
28
|
},
|
|
28
29
|
"files": [
|
|
29
|
-
"dist"
|
|
30
|
+
"dist",
|
|
31
|
+
"src"
|
|
30
32
|
],
|
|
31
33
|
"sideEffects": [
|
|
32
34
|
"*.css"
|
|
33
35
|
],
|
|
34
36
|
"scripts": {
|
|
35
|
-
"build": "
|
|
37
|
+
"build": "tsup && rm -f tsconfig.build.tsbuildinfo && tsc -p tsconfig.build.json",
|
|
36
38
|
"dev": "tsup --watch",
|
|
37
|
-
"prepublishOnly": "deno task
|
|
39
|
+
"prepublishOnly": "echo 'Use deno task release from root'"
|
|
38
40
|
},
|
|
39
41
|
"dependencies": {
|
|
40
|
-
"@proyecto-viviana/
|
|
41
|
-
"@proyecto-viviana/solidaria": "
|
|
42
|
-
"@proyecto-viviana/solidaria-components": "^0.2.2"
|
|
42
|
+
"@proyecto-viviana/solidaria": "workspace:*",
|
|
43
|
+
"@proyecto-viviana/solidaria-components": "workspace:*"
|
|
43
44
|
},
|
|
44
45
|
"peerDependencies": {
|
|
45
46
|
"solid-js": "^1.9.0"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { JSX } from 'solid-js'
|
|
2
|
+
import { Show } from 'solid-js'
|
|
3
|
+
|
|
4
|
+
export type AlertVariant = 'info' | 'success' | 'warning' | 'error'
|
|
5
|
+
|
|
6
|
+
export interface AlertProps {
|
|
7
|
+
children: JSX.Element
|
|
8
|
+
variant?: AlertVariant
|
|
9
|
+
title?: string
|
|
10
|
+
dismissible?: boolean
|
|
11
|
+
onDismiss?: () => void
|
|
12
|
+
class?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const variantStyles: Record<AlertVariant, string> = {
|
|
16
|
+
info: 'bg-primary-700 text-primary-200 border border-primary-500',
|
|
17
|
+
success: 'bg-success-600 text-success-100 border border-success-400',
|
|
18
|
+
warning: 'bg-warning-600 text-warning-100 border border-warning-400',
|
|
19
|
+
error: 'bg-danger-600 text-danger-100 border border-danger-400',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function Alert(props: AlertProps) {
|
|
23
|
+
const variant = () => props.variant ?? 'info'
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div
|
|
27
|
+
class={`flex items-center min-h-[50px] font-normal rounded-lg px-4 py-2 ${variantStyles[variant()]} ${props.class ?? ''}`}
|
|
28
|
+
role="alert"
|
|
29
|
+
>
|
|
30
|
+
<div class="flex items-center gap-3 flex-1">
|
|
31
|
+
<Show when={props.title}>
|
|
32
|
+
<span class="font-semibold font-jost">{props.title}</span>
|
|
33
|
+
<span class="opacity-50">|</span>
|
|
34
|
+
</Show>
|
|
35
|
+
<div class="flex-1">{props.children}</div>
|
|
36
|
+
<Show when={props.dismissible}>
|
|
37
|
+
<button
|
|
38
|
+
class="hover:opacity-70 transition-opacity ml-2"
|
|
39
|
+
onClick={props.onDismiss}
|
|
40
|
+
aria-label="Dismiss"
|
|
41
|
+
>
|
|
42
|
+
✕
|
|
43
|
+
</button>
|
|
44
|
+
</Show>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SearchAutocomplete component for proyecto-viviana-ui
|
|
3
|
+
*
|
|
4
|
+
* A styled autocomplete component combining a search input with a
|
|
5
|
+
* filterable dropdown list of options.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { type JSX, splitProps, createMemo, Show, For, createSignal } from 'solid-js'
|
|
9
|
+
import {
|
|
10
|
+
Autocomplete,
|
|
11
|
+
useAutocompleteInput,
|
|
12
|
+
useAutocompleteCollection,
|
|
13
|
+
useAutocompleteState,
|
|
14
|
+
} from '@proyecto-viviana/solidaria-components'
|
|
15
|
+
|
|
16
|
+
// ============================================
|
|
17
|
+
// TYPES
|
|
18
|
+
// ============================================
|
|
19
|
+
|
|
20
|
+
export type SearchAutocompleteSize = 'sm' | 'md' | 'lg'
|
|
21
|
+
|
|
22
|
+
export interface SearchAutocompleteItem {
|
|
23
|
+
id: string
|
|
24
|
+
name: string
|
|
25
|
+
[key: string]: unknown
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SearchAutocompleteProps<T extends SearchAutocompleteItem = SearchAutocompleteItem> {
|
|
29
|
+
/** The items to display in the dropdown. */
|
|
30
|
+
items: T[]
|
|
31
|
+
/** The size of the autocomplete. @default 'md' */
|
|
32
|
+
size?: SearchAutocompleteSize
|
|
33
|
+
/** Placeholder text for the input. */
|
|
34
|
+
placeholder?: string
|
|
35
|
+
/** Accessible label for the input. */
|
|
36
|
+
'aria-label'?: string
|
|
37
|
+
/** Label text shown above the input. */
|
|
38
|
+
label?: string
|
|
39
|
+
/** Description text shown below the input. */
|
|
40
|
+
description?: string
|
|
41
|
+
/** The current input value (controlled). */
|
|
42
|
+
inputValue?: string
|
|
43
|
+
/** The default input value (uncontrolled). */
|
|
44
|
+
defaultInputValue?: string
|
|
45
|
+
/** Handler called when the input value changes. */
|
|
46
|
+
onInputChange?: (value: string) => void
|
|
47
|
+
/** Handler called when an item is selected. */
|
|
48
|
+
onSelect?: (item: T) => void
|
|
49
|
+
/** Additional CSS class name. */
|
|
50
|
+
class?: string
|
|
51
|
+
/** Whether the input is disabled. */
|
|
52
|
+
isDisabled?: boolean
|
|
53
|
+
/**
|
|
54
|
+
* Custom filter function. By default, filters by case-insensitive name match.
|
|
55
|
+
*/
|
|
56
|
+
filter?: (textValue: string, inputValue: string) => boolean
|
|
57
|
+
/**
|
|
58
|
+
* Custom render function for items.
|
|
59
|
+
*/
|
|
60
|
+
renderItem?: (item: T) => JSX.Element
|
|
61
|
+
/**
|
|
62
|
+
* Key to use for the display text. @default 'name'
|
|
63
|
+
*/
|
|
64
|
+
textKey?: keyof T
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ============================================
|
|
68
|
+
// STYLES
|
|
69
|
+
// ============================================
|
|
70
|
+
|
|
71
|
+
const sizeStyles = {
|
|
72
|
+
sm: {
|
|
73
|
+
container: 'text-sm',
|
|
74
|
+
input: 'h-8 px-3 text-sm',
|
|
75
|
+
label: 'text-xs mb-1',
|
|
76
|
+
list: 'max-h-48',
|
|
77
|
+
item: 'px-3 py-1.5 text-sm',
|
|
78
|
+
},
|
|
79
|
+
md: {
|
|
80
|
+
container: 'text-base',
|
|
81
|
+
input: 'h-10 px-4 text-base',
|
|
82
|
+
label: 'text-sm mb-1.5',
|
|
83
|
+
list: 'max-h-64',
|
|
84
|
+
item: 'px-4 py-2 text-base',
|
|
85
|
+
},
|
|
86
|
+
lg: {
|
|
87
|
+
container: 'text-lg',
|
|
88
|
+
input: 'h-12 px-5 text-lg',
|
|
89
|
+
label: 'text-base mb-2',
|
|
90
|
+
list: 'max-h-80',
|
|
91
|
+
item: 'px-5 py-2.5 text-lg',
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ============================================
|
|
96
|
+
// INNER COMPONENTS
|
|
97
|
+
// ============================================
|
|
98
|
+
|
|
99
|
+
function AutocompleteInput(props: {
|
|
100
|
+
placeholder?: string
|
|
101
|
+
'aria-label'?: string
|
|
102
|
+
isDisabled?: boolean
|
|
103
|
+
size: SearchAutocompleteSize
|
|
104
|
+
}) {
|
|
105
|
+
const ctx = useAutocompleteInput()
|
|
106
|
+
if (!ctx) return null
|
|
107
|
+
|
|
108
|
+
const styles = () => sizeStyles[props.size]
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<input
|
|
112
|
+
ref={ctx.inputRef}
|
|
113
|
+
type="text"
|
|
114
|
+
placeholder={props.placeholder}
|
|
115
|
+
aria-label={props['aria-label']}
|
|
116
|
+
disabled={props.isDisabled}
|
|
117
|
+
value={ctx.inputProps.value()}
|
|
118
|
+
onInput={(e) => ctx.inputProps.onChange(e.currentTarget.value)}
|
|
119
|
+
onKeyDown={ctx.inputProps.onKeyDown}
|
|
120
|
+
onFocus={ctx.inputProps.onFocus}
|
|
121
|
+
onBlur={ctx.inputProps.onBlur}
|
|
122
|
+
aria-activedescendant={ctx.inputProps['aria-activedescendant']()}
|
|
123
|
+
aria-controls={ctx.inputProps['aria-controls']}
|
|
124
|
+
aria-autocomplete={ctx.inputProps['aria-autocomplete']}
|
|
125
|
+
autocomplete={ctx.inputProps.autoComplete}
|
|
126
|
+
autocorrect={ctx.inputProps.autoCorrect}
|
|
127
|
+
spellcheck={ctx.inputProps.spellCheck !== 'false'}
|
|
128
|
+
class={[
|
|
129
|
+
'w-full rounded-md border border-bg-200 bg-bg-50',
|
|
130
|
+
'focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500',
|
|
131
|
+
'placeholder:text-text-400',
|
|
132
|
+
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
133
|
+
styles().input,
|
|
134
|
+
].join(' ')}
|
|
135
|
+
/>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function AutocompleteList<T extends SearchAutocompleteItem>(props: {
|
|
140
|
+
items: T[]
|
|
141
|
+
size: SearchAutocompleteSize
|
|
142
|
+
onSelect?: (item: T) => void
|
|
143
|
+
renderItem?: (item: T) => JSX.Element
|
|
144
|
+
textKey: keyof T
|
|
145
|
+
}) {
|
|
146
|
+
const ctx = useAutocompleteCollection()
|
|
147
|
+
const state = useAutocompleteState()
|
|
148
|
+
if (!ctx) return null
|
|
149
|
+
|
|
150
|
+
const styles = () => sizeStyles[props.size]
|
|
151
|
+
|
|
152
|
+
// Filter items based on input
|
|
153
|
+
const filteredItems = createMemo(() => {
|
|
154
|
+
if (!ctx.filter) return props.items
|
|
155
|
+
return props.items.filter((item) => {
|
|
156
|
+
const textValue = String(item[props.textKey] ?? item.name ?? '')
|
|
157
|
+
return ctx.filter!(textValue)
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
const handleSelect = (item: T) => {
|
|
162
|
+
props.onSelect?.(item)
|
|
163
|
+
state?.setInputValue(String(item[props.textKey] ?? item.name ?? ''))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<Show when={filteredItems().length > 0}>
|
|
168
|
+
<ul
|
|
169
|
+
ref={ctx.collectionRef}
|
|
170
|
+
id={ctx.collectionProps.id}
|
|
171
|
+
role="listbox"
|
|
172
|
+
aria-label={ctx.collectionProps['aria-label']}
|
|
173
|
+
class={[
|
|
174
|
+
'mt-1 w-full rounded-md border border-bg-200 bg-bg-50 shadow-lg',
|
|
175
|
+
'overflow-auto',
|
|
176
|
+
styles().list,
|
|
177
|
+
].join(' ')}
|
|
178
|
+
>
|
|
179
|
+
<For each={filteredItems()}>
|
|
180
|
+
{(item) => {
|
|
181
|
+
const itemId = `autocomplete-item-${item.id}`
|
|
182
|
+
const isFocused = () => state?.focusedNodeId() === itemId
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<li
|
|
186
|
+
id={itemId}
|
|
187
|
+
role="option"
|
|
188
|
+
aria-selected={isFocused()}
|
|
189
|
+
onClick={() => handleSelect(item)}
|
|
190
|
+
onMouseEnter={() => state?.setFocusedNodeId(itemId)}
|
|
191
|
+
onMouseLeave={() => {
|
|
192
|
+
if (state?.focusedNodeId() === itemId) {
|
|
193
|
+
state?.setFocusedNodeId(null)
|
|
194
|
+
}
|
|
195
|
+
}}
|
|
196
|
+
class={[
|
|
197
|
+
'cursor-pointer transition-colors',
|
|
198
|
+
isFocused()
|
|
199
|
+
? 'bg-primary-100 text-primary-900'
|
|
200
|
+
: 'hover:bg-bg-100',
|
|
201
|
+
styles().item,
|
|
202
|
+
].join(' ')}
|
|
203
|
+
>
|
|
204
|
+
{props.renderItem ? props.renderItem(item) : String(item[props.textKey] ?? item.name)}
|
|
205
|
+
</li>
|
|
206
|
+
)
|
|
207
|
+
}}
|
|
208
|
+
</For>
|
|
209
|
+
</ul>
|
|
210
|
+
</Show>
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ============================================
|
|
215
|
+
// SEARCH AUTOCOMPLETE COMPONENT
|
|
216
|
+
// ============================================
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* A styled autocomplete component for searching and selecting from a list.
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```tsx
|
|
223
|
+
* const items = [
|
|
224
|
+
* { id: '1', name: 'Apple' },
|
|
225
|
+
* { id: '2', name: 'Banana' },
|
|
226
|
+
* { id: '3', name: 'Cherry' },
|
|
227
|
+
* ];
|
|
228
|
+
*
|
|
229
|
+
* <SearchAutocomplete
|
|
230
|
+
* items={items}
|
|
231
|
+
* placeholder="Search fruits..."
|
|
232
|
+
* aria-label="Fruit search"
|
|
233
|
+
* onSelect={(item) => console.log('Selected:', item)}
|
|
234
|
+
* />
|
|
235
|
+
*
|
|
236
|
+
* // With custom filter
|
|
237
|
+
* <SearchAutocomplete
|
|
238
|
+
* items={items}
|
|
239
|
+
* filter={(textValue, inputValue) =>
|
|
240
|
+
* textValue.toLowerCase().startsWith(inputValue.toLowerCase())
|
|
241
|
+
* }
|
|
242
|
+
* onSelect={(item) => console.log('Selected:', item)}
|
|
243
|
+
* />
|
|
244
|
+
*
|
|
245
|
+
* // With label and description
|
|
246
|
+
* <SearchAutocomplete
|
|
247
|
+
* items={items}
|
|
248
|
+
* label="Search"
|
|
249
|
+
* description="Type to filter the list"
|
|
250
|
+
* placeholder="Start typing..."
|
|
251
|
+
* />
|
|
252
|
+
* ```
|
|
253
|
+
*/
|
|
254
|
+
export function SearchAutocomplete<T extends SearchAutocompleteItem = SearchAutocompleteItem>(
|
|
255
|
+
props: SearchAutocompleteProps<T>
|
|
256
|
+
): JSX.Element {
|
|
257
|
+
const [local, autocompleteProps] = splitProps(props, [
|
|
258
|
+
'items',
|
|
259
|
+
'size',
|
|
260
|
+
'placeholder',
|
|
261
|
+
'aria-label',
|
|
262
|
+
'label',
|
|
263
|
+
'description',
|
|
264
|
+
'onSelect',
|
|
265
|
+
'class',
|
|
266
|
+
'isDisabled',
|
|
267
|
+
'renderItem',
|
|
268
|
+
'textKey',
|
|
269
|
+
])
|
|
270
|
+
|
|
271
|
+
const size = () => local.size ?? 'md'
|
|
272
|
+
const textKey = () => local.textKey ?? 'name'
|
|
273
|
+
const styles = () => sizeStyles[size()]
|
|
274
|
+
|
|
275
|
+
// Default filter: case-insensitive contains
|
|
276
|
+
const defaultFilter = (textValue: string, inputValue: string) => {
|
|
277
|
+
if (!inputValue) return true
|
|
278
|
+
return textValue.toLowerCase().includes(inputValue.toLowerCase())
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<div class={['vui-search-autocomplete relative', styles().container, local.class].filter(Boolean).join(' ')}>
|
|
283
|
+
<Show when={local.label}>
|
|
284
|
+
<label class={['block font-medium text-text-700', styles().label].join(' ')}>
|
|
285
|
+
{local.label}
|
|
286
|
+
</label>
|
|
287
|
+
</Show>
|
|
288
|
+
|
|
289
|
+
<Autocomplete
|
|
290
|
+
{...autocompleteProps}
|
|
291
|
+
filter={autocompleteProps.filter ?? defaultFilter}
|
|
292
|
+
>
|
|
293
|
+
<AutocompleteInput
|
|
294
|
+
placeholder={local.placeholder}
|
|
295
|
+
aria-label={local['aria-label']}
|
|
296
|
+
isDisabled={local.isDisabled}
|
|
297
|
+
size={size()}
|
|
298
|
+
/>
|
|
299
|
+
<AutocompleteList
|
|
300
|
+
items={local.items}
|
|
301
|
+
size={size()}
|
|
302
|
+
onSelect={local.onSelect}
|
|
303
|
+
renderItem={local.renderItem}
|
|
304
|
+
textKey={textKey() as keyof T}
|
|
305
|
+
/>
|
|
306
|
+
</Autocomplete>
|
|
307
|
+
|
|
308
|
+
<Show when={local.description}>
|
|
309
|
+
<p class="mt-1 text-sm text-text-500">{local.description}</p>
|
|
310
|
+
</Show>
|
|
311
|
+
</div>
|
|
312
|
+
)
|
|
313
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Show } from 'solid-js'
|
|
2
|
+
|
|
3
|
+
export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
|
4
|
+
|
|
5
|
+
export interface AvatarProps {
|
|
6
|
+
src?: string
|
|
7
|
+
alt?: string
|
|
8
|
+
size?: AvatarSize
|
|
9
|
+
fallback?: string
|
|
10
|
+
online?: boolean
|
|
11
|
+
class?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const sizeStyles: Record<AvatarSize, { container: string; text: string; indicator: string }> = {
|
|
15
|
+
xs: { container: 'w-6 h-6', text: 'text-xs', indicator: 'w-1.5 h-1.5' },
|
|
16
|
+
sm: { container: 'w-8 h-8', text: 'text-sm', indicator: 'w-2 h-2' },
|
|
17
|
+
md: { container: 'w-10 h-10', text: 'text-base', indicator: 'w-2.5 h-2.5' },
|
|
18
|
+
lg: { container: 'w-14 h-14', text: 'text-lg', indicator: 'w-3 h-3' },
|
|
19
|
+
xl: { container: 'w-20 h-20', text: 'text-xl', indicator: 'w-4 h-4' },
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function Avatar(props: AvatarProps) {
|
|
23
|
+
const size = () => props.size ?? 'md'
|
|
24
|
+
const styles = () => sizeStyles[size()]
|
|
25
|
+
|
|
26
|
+
const initials = () => {
|
|
27
|
+
if (props.fallback) return props.fallback.slice(0, 2).toUpperCase()
|
|
28
|
+
if (props.alt) return props.alt.slice(0, 2).toUpperCase()
|
|
29
|
+
return '?'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div class={`relative inline-block ${props.class ?? ''}`}>
|
|
34
|
+
<div
|
|
35
|
+
class={`${styles().container} rounded-full overflow-hidden bg-bg-200 flex items-center justify-center ring-2 ring-accent/50`}
|
|
36
|
+
>
|
|
37
|
+
<Show
|
|
38
|
+
when={props.src}
|
|
39
|
+
fallback={
|
|
40
|
+
<span class={`${styles().text} font-medium text-primary-300`}>
|
|
41
|
+
{initials()}
|
|
42
|
+
</span>
|
|
43
|
+
}
|
|
44
|
+
>
|
|
45
|
+
<img
|
|
46
|
+
src={props.src}
|
|
47
|
+
alt={props.alt ?? 'Avatar'}
|
|
48
|
+
class="w-full h-full object-cover"
|
|
49
|
+
/>
|
|
50
|
+
</Show>
|
|
51
|
+
</div>
|
|
52
|
+
<Show when={props.online !== undefined}>
|
|
53
|
+
<span
|
|
54
|
+
class={`absolute bottom-0 right-0 ${styles().indicator} rounded-full ring-2 ring-bg-400 ${
|
|
55
|
+
props.online ? 'bg-success-400' : 'bg-bg-light'
|
|
56
|
+
}`}
|
|
57
|
+
/>
|
|
58
|
+
</Show>
|
|
59
|
+
</div>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface AvatarGroupProps {
|
|
64
|
+
children: any
|
|
65
|
+
max?: number
|
|
66
|
+
size?: AvatarSize
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function AvatarGroup(props: AvatarGroupProps) {
|
|
70
|
+
return (
|
|
71
|
+
<div class="flex -space-x-2">
|
|
72
|
+
{props.children}
|
|
73
|
+
</div>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { JSX } from 'solid-js'
|
|
2
|
+
import { Show } from 'solid-js'
|
|
3
|
+
|
|
4
|
+
export type BadgeVariant = 'primary' | 'secondary' | 'accent' | 'success' | 'warning' | 'danger'
|
|
5
|
+
export type BadgeSize = 'sm' | 'md' | 'lg'
|
|
6
|
+
|
|
7
|
+
export interface BadgeProps {
|
|
8
|
+
children?: JSX.Element
|
|
9
|
+
count?: number
|
|
10
|
+
variant?: BadgeVariant
|
|
11
|
+
size?: BadgeSize
|
|
12
|
+
class?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const variantStyles: Record<BadgeVariant, string> = {
|
|
16
|
+
primary: 'bg-primary-500 text-white',
|
|
17
|
+
secondary: 'bg-bg-300 text-primary-300',
|
|
18
|
+
accent: 'bg-accent-300 text-black',
|
|
19
|
+
success: 'bg-success-400 text-white',
|
|
20
|
+
warning: 'bg-warning-400 text-black',
|
|
21
|
+
danger: 'bg-danger-400 text-white',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const sizeStyles: Record<BadgeSize, string> = {
|
|
25
|
+
sm: 'w-5 h-5 text-xs',
|
|
26
|
+
md: 'w-7 h-7 text-xs',
|
|
27
|
+
lg: 'w-9 h-9 text-sm',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function Badge(props: BadgeProps) {
|
|
31
|
+
const variant = () => props.variant ?? 'accent'
|
|
32
|
+
const size = () => props.size ?? 'md'
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
class={`flex items-center justify-center rounded-full border-b border-white font-semibold ${variantStyles[variant()]} ${sizeStyles[size()]} ${props.class ?? ''}`}
|
|
37
|
+
>
|
|
38
|
+
<Show when={props.count !== undefined} fallback={props.children}>
|
|
39
|
+
<span>{props.count}</span>
|
|
40
|
+
</Show>
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
}
|