@tristan2828/ui-foundation 4.51.0 → 4.52.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.
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type EmojiEntry = {
|
|
2
|
+
emoji: string;
|
|
3
|
+
name: string;
|
|
4
|
+
keywords?: string;
|
|
5
|
+
};
|
|
6
|
+
export type EmojiGroup = {
|
|
7
|
+
label: string;
|
|
8
|
+
emojis: EmojiEntry[];
|
|
9
|
+
};
|
|
10
|
+
export declare const EMOJI_GROUPS: EmojiGroup[];
|
|
11
|
+
export declare function filterEmojiGroups(query: string): EmojiGroup[];
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
export const EMOJI_GROUPS = [
|
|
2
|
+
{
|
|
3
|
+
label: 'Morning and sleep',
|
|
4
|
+
emojis: [
|
|
5
|
+
{ emoji: '☀️', name: 'Sun', keywords: 'morning day bright' },
|
|
6
|
+
{ emoji: '🌅', name: 'Sunrise', keywords: 'morning dawn' },
|
|
7
|
+
{ emoji: '⏰', name: 'Alarm clock', keywords: 'wake time' },
|
|
8
|
+
{ emoji: '🌙', name: 'Moon', keywords: 'night evening' },
|
|
9
|
+
{ emoji: '😴', name: 'Sleeping face', keywords: 'sleep bed rest' },
|
|
10
|
+
{ emoji: '🛏️', name: 'Bed', keywords: 'sleep bedroom' },
|
|
11
|
+
{ emoji: '🌄', name: 'Sunrise over mountains', keywords: 'morning' },
|
|
12
|
+
{ emoji: '🌃', name: 'Night with stars', keywords: 'evening' },
|
|
13
|
+
],
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
label: 'Body and health',
|
|
17
|
+
emojis: [
|
|
18
|
+
{ emoji: '🧘', name: 'Person in lotus position', keywords: 'yoga meditate breathe calm' },
|
|
19
|
+
{ emoji: '🏃', name: 'Person running', keywords: 'run exercise workout cardio' },
|
|
20
|
+
{ emoji: '🏋️', name: 'Person lifting weights', keywords: 'gym strength workout' },
|
|
21
|
+
{ emoji: '🚴', name: 'Person biking', keywords: 'cycling exercise' },
|
|
22
|
+
{ emoji: '🏊', name: 'Person swimming', keywords: 'pool exercise' },
|
|
23
|
+
{ emoji: '🤸', name: 'Person cartwheeling', keywords: 'stretch gymnastics' },
|
|
24
|
+
{ emoji: '💪', name: 'Flexed biceps', keywords: 'strength muscle' },
|
|
25
|
+
{ emoji: '🦷', name: 'Tooth', keywords: 'dentist brush teeth' },
|
|
26
|
+
{ emoji: '🪥', name: 'Toothbrush', keywords: 'brush teeth' },
|
|
27
|
+
{ emoji: '💊', name: 'Pill', keywords: 'medicine vitamins' },
|
|
28
|
+
{ emoji: '🩺', name: 'Stethoscope', keywords: 'doctor health' },
|
|
29
|
+
{ emoji: '❤️', name: 'Red heart', keywords: 'love health' },
|
|
30
|
+
{ emoji: '🫁', name: 'Lungs', keywords: 'breathe breath' },
|
|
31
|
+
{ emoji: '🧠', name: 'Brain', keywords: 'mind think' },
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
label: 'Self-care',
|
|
36
|
+
emojis: [
|
|
37
|
+
{ emoji: '🚿', name: 'Shower', keywords: 'wash clean' },
|
|
38
|
+
{ emoji: '🛁', name: 'Bathtub', keywords: 'bath wash' },
|
|
39
|
+
{ emoji: '🧼', name: 'Soap', keywords: 'wash clean hands' },
|
|
40
|
+
{ emoji: '🧴', name: 'Lotion bottle', keywords: 'skincare moisturizer sunscreen' },
|
|
41
|
+
{ emoji: '🪒', name: 'Razor', keywords: 'shave' },
|
|
42
|
+
{ emoji: '💇', name: 'Person getting haircut', keywords: 'hair' },
|
|
43
|
+
{ emoji: '🧖', name: 'Person in steamy room', keywords: 'sauna spa relax' },
|
|
44
|
+
{ emoji: '🕯️', name: 'Candle', keywords: 'calm relax' },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
label: 'Food and drink',
|
|
49
|
+
emojis: [
|
|
50
|
+
{ emoji: '🍳', name: 'Cooking', keywords: 'breakfast egg pan' },
|
|
51
|
+
{ emoji: '🥣', name: 'Bowl with spoon', keywords: 'cereal oatmeal breakfast' },
|
|
52
|
+
{ emoji: '🥗', name: 'Green salad', keywords: 'lunch healthy' },
|
|
53
|
+
{ emoji: '🍽️', name: 'Plate with cutlery', keywords: 'meal dinner eat' },
|
|
54
|
+
{ emoji: '🍎', name: 'Red apple', keywords: 'fruit snack' },
|
|
55
|
+
{ emoji: '🥑', name: 'Avocado', keywords: 'food' },
|
|
56
|
+
{ emoji: '☕', name: 'Hot beverage', keywords: 'coffee tea' },
|
|
57
|
+
{ emoji: '🍵', name: 'Teacup', keywords: 'tea' },
|
|
58
|
+
{ emoji: '💧', name: 'Droplet', keywords: 'water drink hydrate' },
|
|
59
|
+
{ emoji: '🥤', name: 'Cup with straw', keywords: 'drink' },
|
|
60
|
+
{ emoji: '🛒', name: 'Shopping cart', keywords: 'groceries store' },
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
label: 'Home and chores',
|
|
65
|
+
emojis: [
|
|
66
|
+
{ emoji: '🏠', name: 'House', keywords: 'home' },
|
|
67
|
+
{ emoji: '🧹', name: 'Broom', keywords: 'sweep clean chores' },
|
|
68
|
+
{ emoji: '🧽', name: 'Sponge', keywords: 'scrub clean dishes' },
|
|
69
|
+
{ emoji: '🧺', name: 'Basket', keywords: 'laundry' },
|
|
70
|
+
{ emoji: '👕', name: 'T-shirt', keywords: 'laundry clothes' },
|
|
71
|
+
{ emoji: '🗑️', name: 'Wastebasket', keywords: 'trash garbage' },
|
|
72
|
+
{ emoji: '🛠️', name: 'Hammer and wrench', keywords: 'repair fix tools' },
|
|
73
|
+
{ emoji: '🔧', name: 'Wrench', keywords: 'repair fix' },
|
|
74
|
+
{ emoji: '🔑', name: 'Key', keywords: 'lock keys' },
|
|
75
|
+
{ emoji: '🪴', name: 'Potted plant', keywords: 'garden water plants' },
|
|
76
|
+
{ emoji: '🌱', name: 'Seedling', keywords: 'garden grow plant' },
|
|
77
|
+
{ emoji: '🌿', name: 'Herb', keywords: 'garden plant' },
|
|
78
|
+
{ emoji: '🐕', name: 'Dog', keywords: 'pet walk' },
|
|
79
|
+
{ emoji: '🐈', name: 'Cat', keywords: 'pet' },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
label: 'Work and learning',
|
|
84
|
+
emojis: [
|
|
85
|
+
{ emoji: '💻', name: 'Laptop', keywords: 'computer work code' },
|
|
86
|
+
{ emoji: '📧', name: 'Email', keywords: 'mail inbox' },
|
|
87
|
+
{ emoji: '📅', name: 'Calendar', keywords: 'date plan schedule' },
|
|
88
|
+
{ emoji: '📝', name: 'Memo', keywords: 'notes write journal' },
|
|
89
|
+
{ emoji: '✅', name: 'Check mark', keywords: 'done task todo' },
|
|
90
|
+
{ emoji: '📋', name: 'Clipboard', keywords: 'list checklist' },
|
|
91
|
+
{ emoji: '📚', name: 'Books', keywords: 'read study learn' },
|
|
92
|
+
{ emoji: '📖', name: 'Open book', keywords: 'read' },
|
|
93
|
+
{ emoji: '🎓', name: 'Graduation cap', keywords: 'study school learn' },
|
|
94
|
+
{ emoji: '✏️', name: 'Pencil', keywords: 'write draw' },
|
|
95
|
+
{ emoji: '🎯', name: 'Bullseye', keywords: 'goal focus target' },
|
|
96
|
+
{ emoji: '💡', name: 'Light bulb', keywords: 'idea' },
|
|
97
|
+
{ emoji: '🧾', name: 'Receipt', keywords: 'bills budget' },
|
|
98
|
+
{ emoji: '💰', name: 'Money bag', keywords: 'finance budget savings' },
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
label: 'Play and music',
|
|
103
|
+
emojis: [
|
|
104
|
+
{ emoji: '🎵', name: 'Musical note', keywords: 'music song' },
|
|
105
|
+
{ emoji: '🎧', name: 'Headphones', keywords: 'music podcast listen' },
|
|
106
|
+
{ emoji: '🎸', name: 'Guitar', keywords: 'music practice' },
|
|
107
|
+
{ emoji: '🎹', name: 'Piano keyboard', keywords: 'music practice' },
|
|
108
|
+
{ emoji: '🎨', name: 'Artist palette', keywords: 'paint draw art' },
|
|
109
|
+
{ emoji: '🎮', name: 'Video game', keywords: 'play controller' },
|
|
110
|
+
{ emoji: '📺', name: 'Television', keywords: 'tv watch show' },
|
|
111
|
+
{ emoji: '🎬', name: 'Clapper board', keywords: 'movie film' },
|
|
112
|
+
{ emoji: '📷', name: 'Camera', keywords: 'photo picture' },
|
|
113
|
+
{ emoji: '🧩', name: 'Puzzle piece', keywords: 'game' },
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
label: 'Outside and travel',
|
|
118
|
+
emojis: [
|
|
119
|
+
{ emoji: '🚶', name: 'Person walking', keywords: 'walk steps' },
|
|
120
|
+
{ emoji: '🥾', name: 'Hiking boot', keywords: 'hike trail' },
|
|
121
|
+
{ emoji: '🌳', name: 'Tree', keywords: 'park nature' },
|
|
122
|
+
{ emoji: '⛰️', name: 'Mountain', keywords: 'hike' },
|
|
123
|
+
{ emoji: '🌧️', name: 'Rain cloud', keywords: 'weather' },
|
|
124
|
+
{ emoji: '❄️', name: 'Snowflake', keywords: 'winter cold' },
|
|
125
|
+
{ emoji: '🚗', name: 'Car', keywords: 'drive commute' },
|
|
126
|
+
{ emoji: '🚌', name: 'Bus', keywords: 'commute transit' },
|
|
127
|
+
{ emoji: '✈️', name: 'Airplane', keywords: 'travel flight trip' },
|
|
128
|
+
{ emoji: '🧳', name: 'Luggage', keywords: 'travel pack trip' },
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
label: 'Symbols',
|
|
133
|
+
emojis: [
|
|
134
|
+
{ emoji: '⭐', name: 'Star', keywords: 'favorite' },
|
|
135
|
+
{ emoji: '✨', name: 'Sparkles', keywords: 'new clean' },
|
|
136
|
+
{ emoji: '🔥', name: 'Fire', keywords: 'streak hot' },
|
|
137
|
+
{ emoji: '🎉', name: 'Party popper', keywords: 'celebrate' },
|
|
138
|
+
{ emoji: '🏆', name: 'Trophy', keywords: 'win goal' },
|
|
139
|
+
{ emoji: '🔔', name: 'Bell', keywords: 'reminder notify' },
|
|
140
|
+
{ emoji: '⏱️', name: 'Stopwatch', keywords: 'timer time' },
|
|
141
|
+
{ emoji: '⏳', name: 'Hourglass', keywords: 'wait time' },
|
|
142
|
+
{ emoji: '🔄', name: 'Counterclockwise arrows', keywords: 'repeat again refresh' },
|
|
143
|
+
{ emoji: '🙏', name: 'Folded hands', keywords: 'gratitude thanks pray' },
|
|
144
|
+
{ emoji: '😊', name: 'Smiling face', keywords: 'happy' },
|
|
145
|
+
{ emoji: '🌈', name: 'Rainbow', keywords: 'colour' },
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
];
|
|
149
|
+
export function filterEmojiGroups(query) {
|
|
150
|
+
const q = query.trim().toLowerCase();
|
|
151
|
+
if (!q)
|
|
152
|
+
return EMOJI_GROUPS;
|
|
153
|
+
return EMOJI_GROUPS.map((group) => ({
|
|
154
|
+
...group,
|
|
155
|
+
emojis: group.emojis.filter((e) => `${e.name} ${e.keywords ?? ''}`.toLowerCase().includes(q)),
|
|
156
|
+
})).filter((group) => group.emojis.length > 0);
|
|
157
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type EmojiFieldProps = {
|
|
2
|
+
/** The trigger button's id, for the field's `<FieldLabel htmlFor>`. */
|
|
3
|
+
id?: string;
|
|
4
|
+
/** The emoji, or `''` for none. */
|
|
5
|
+
value: string;
|
|
6
|
+
/** Called with the picked emoji, or `''` when cleared. */
|
|
7
|
+
onChange: (value: string) => void;
|
|
8
|
+
/** What the button says with none picked. Defaults to "Pick an emoji". */
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
/** Offer Clear in the popup, for an optional field. */
|
|
11
|
+
clearable?: boolean;
|
|
12
|
+
/** The value was refused: `aria-invalid` on the button. */
|
|
13
|
+
invalid?: boolean;
|
|
14
|
+
/** Saving: the button shows the emoji but won't open. */
|
|
15
|
+
readOnly?: boolean;
|
|
16
|
+
/** Open on mount (for stories and editing in place). */
|
|
17
|
+
defaultOpen?: boolean;
|
|
18
|
+
className?: string;
|
|
19
|
+
'aria-label'?: string;
|
|
20
|
+
'aria-labelledby'?: string;
|
|
21
|
+
'aria-describedby'?: string;
|
|
22
|
+
};
|
|
23
|
+
export declare function EmojiField({ id, value, onChange, placeholder, clearable, invalid, readOnly, defaultOpen, className, ...aria }: EmojiFieldProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// One emoji on a form: a routine's mark, a list's icon. The value is the
|
|
3
|
+
// emoji itself, or `''` for none. A button showing the choice, opening a
|
|
4
|
+
// Popover with a search box and a curated set of emoji in groups (not the
|
|
5
|
+
// whole of Unicode: `emoji-field-data.ts`). Like DateField it is one
|
|
6
|
+
// control for a react-hook-form `Controller`; `clearable` adds Clear.
|
|
7
|
+
import { useState } from 'react';
|
|
8
|
+
import { SmilePlus } from 'lucide-react';
|
|
9
|
+
import { cn } from 'cn';
|
|
10
|
+
import { Button } from '../ui/button.js';
|
|
11
|
+
import { Input } from '../ui/input.js';
|
|
12
|
+
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover.js';
|
|
13
|
+
import { filterEmojiGroups } from './emoji-field-data.js';
|
|
14
|
+
export function EmojiField({ id, value, onChange, placeholder = 'Pick an emoji', clearable = false, invalid = false, readOnly = false, defaultOpen = false, className, ...aria }) {
|
|
15
|
+
const [open, setOpen] = useState(defaultOpen && !readOnly);
|
|
16
|
+
const [query, setQuery] = useState('');
|
|
17
|
+
const groups = filterEmojiGroups(query);
|
|
18
|
+
const changeOpen = (next) => {
|
|
19
|
+
if (next && readOnly)
|
|
20
|
+
return;
|
|
21
|
+
setOpen(next);
|
|
22
|
+
if (!next)
|
|
23
|
+
setQuery('');
|
|
24
|
+
};
|
|
25
|
+
const pick = (next) => {
|
|
26
|
+
onChange(next);
|
|
27
|
+
changeOpen(false);
|
|
28
|
+
};
|
|
29
|
+
return (_jsxs(Popover, { open: open, onOpenChange: changeOpen, children: [_jsx(PopoverTrigger, { render: _jsx(Button, { id: id, type: "button", variant: "outline", "aria-invalid": invalid || undefined, "aria-readonly": readOnly || undefined, className: cn('w-full justify-start font-normal', !value && 'text-muted-foreground', className), ...aria }), children: value ? (_jsx("span", { className: "text-base leading-none", children: value })) : (_jsxs(_Fragment, { children: [_jsx(SmilePlus, { "aria-hidden": "true", className: "size-4" }), placeholder] })) }), _jsxs(PopoverContent, { className: "w-72 gap-2 p-2", align: "start", "data-slot": "emoji-field-popup", "aria-label": "Emoji", children: [_jsx(Input, { type: "search", autoComplete: "off", "aria-label": "Search emoji", placeholder: "Search", value: query, onChange: (event) => setQuery(event.target.value) }), _jsx("div", { className: "max-h-64 overflow-y-auto", children: groups.length === 0 ? (_jsxs("p", { className: "p-2 text-sm text-muted-foreground", children: ["No emoji match \"", query.trim(), "\"."] })) : (groups.map((group) => (_jsxs("section", { "aria-label": group.label, className: "mb-2 last:mb-0", children: [_jsx("h3", { className: "px-1 py-1 text-xs font-medium text-muted-foreground", children: group.label }), _jsx("div", { className: "grid grid-cols-7 gap-0.5", children: group.emojis.map((entry) => (_jsx("button", { type: "button", "aria-label": entry.name, title: entry.name, "aria-pressed": entry.emoji === value, onClick: () => pick(entry.emoji), className: "flex size-8 items-center justify-center rounded-md text-xl leading-none outline-none hover:bg-muted focus-visible:ring-3 focus-visible:ring-ring/50 aria-pressed:bg-accent", children: entry.emoji }, entry.emoji))) })] }, group.label)))) }), clearable && value && (_jsx("div", { className: "border-t border-border pt-2", children: _jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "w-full", onClick: () => pick(''), children: "Clear" }) }))] })] }));
|
|
30
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -41,6 +41,7 @@ export { MultiLineChart, type ChartSeries, type ChartTone, type MultiLineChartPr
|
|
|
41
41
|
export { StepBar, type StepBarProps } from './components/app/step-bar.js';
|
|
42
42
|
export { RatingStar, type RatingStarProps, type RatingStarValue } from './components/app/rating-star.js';
|
|
43
43
|
export { DateField, type DateFieldProps } from './components/app/date-field.js';
|
|
44
|
+
export { EmojiField, type EmojiFieldProps } from './components/app/emoji-field.js';
|
|
44
45
|
export { ImageField, type ImageFieldProps } from './components/app/image-field.js';
|
|
45
46
|
export { DEFAULT_IMAGE_TYPES, type ImageUpload, type ImageUploadResult } from './lib/image-file.js';
|
|
46
47
|
export { formatPlainDate, type PlainDate } from './lib/plain-date.js';
|
package/dist/index.js
CHANGED
|
@@ -44,6 +44,7 @@ export { MultiLineChart, } from './components/app/multi-line-chart.js';
|
|
|
44
44
|
export { StepBar } from './components/app/step-bar.js';
|
|
45
45
|
export { RatingStar } from './components/app/rating-star.js';
|
|
46
46
|
export { DateField } from './components/app/date-field.js';
|
|
47
|
+
export { EmojiField } from './components/app/emoji-field.js';
|
|
47
48
|
export { ImageField } from './components/app/image-field.js';
|
|
48
49
|
export { DEFAULT_IMAGE_TYPES } from './lib/image-file.js';
|
|
49
50
|
export { formatPlainDate } from './lib/plain-date.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tristan2828/ui-foundation",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.52.0",
|
|
4
4
|
"description": "Shared UI foundation for contract-first, database-backed React apps: shadcn primitives, DataTable/EntityForm composites, auth, the gateway error seam, design tokens, lint rules and the AI playbook.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|