Skip to content

Commit 1b9b7cd

Browse files
Development (#250)
* Feature/throttle updates and responsive bug (#241) * Fixing throttle updates and resposnive issue * Updating package and changelog with locales * Small refactor * Updating changelog * 248 reported delays with card updates (#249) * Updates with timecard issues * Fixed bug where ticking function wouldn't update the clock if no entity or other props are provided * Optimsations with tooltip * updating log * Updating changelog * Reverting time card story changes * Typo * Locales, prettier & bumping version
1 parent 110c2a2 commit 1b9b7cd

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

81 files changed

+97569
-97419
lines changed

.storybook/preview.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,12 @@ export default {
107107
canvas: {
108108
sourceState: 'shown',
109109
},
110+
source: {
111+
dark: true,
112+
language: 'tsx',
113+
excludeDecorators: false,
114+
format: 'dedent',
115+
},
110116
page: Page
111117
}
112118
},

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
# 5.0.9
2+
3+
### @hakit/components
4+
5+
- NEW - TimeCard - when no time/date entity is provided, by default it will use the browsers time and date, this also uses a custom formatter to format the custom date string, previously the custom formatters only allowed you to specify strings per identifier, now you can return a ReactNode meaning we can have the same formatting behavior as if we have an entity (AM/PM suffix now wrapped in different styles.)
6+
- BREAKING - TimeCard - Unlikely breaking change for users unless you're potentially using css selectors targeting the h4 element in time cards, the h4 element is now a span to avoid nesting h4 elements after introducing the above feature.
7+
- IMPROVEMENT - Tooltip - A previously unknown behavior as something changed along the way, all tooltips were rendered on the page even before interacting with elements, and also continuously updating position on window resize, now tooltip elements are only created when interacting with the element with the tooltip, and removed from the dom after the interaction is complete, this should reduce the amount of elements on the page and improve performance.
8+
9+
### @hakit/core
10+
- useEntity - more issues with the useEntity hook causing delays in updates, the hook behind the scenes was using a debounce not a throttle which was not intended behavior, this seems to have resolved syncing issues with storybook and the actual dashboard. [fixes](https://github.com/shannonhochkins/ha-component-kit/issues/248)
11+
12+
113
# 5.0.8
214

315
### @hakit/components

hakit/server/default.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,6 @@ <h2>Write to File</h2>
110110
.catch(error => console.error('Error:', error));
111111
}
112112
function runApplication() {
113-
console.log('xx', `${baseUrl}run-application`);
114113
fetch(`${baseUrl}api/v1/run-application`, {
115114
method: 'POST'
116115
})

hass-connect-fake/index.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -437,14 +437,13 @@ function HassProvider({
437437
last_changed: now.toISOString(),
438438
last_updated: now.toISOString(),
439439
}
440-
setEntities({
441-
['sensor.time']: {
442-
...entities['sensor.time'],
443-
...dates,
444-
state: formatted
445-
}
446-
});
447-
}, 60000);
440+
entities['sensor.time'] = {
441+
...entities['sensor.time'],
442+
...dates,
443+
state: formatted
444+
}
445+
setEntities(entities);
446+
}, 125);
448447
return () => {
449448
if (clock.current) clearInterval(clock.current);
450449
}

packages/components/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@hakit/components",
33
"type": "module",
4-
"version": "5.0.8",
4+
"version": "5.0.9",
55
"private": false,
66
"keywords": [
77
"react",
@@ -69,7 +69,7 @@
6969
"@emotion/react": ">=11.x.x",
7070
"@emotion/styled": ">=11.x",
7171
"@fullcalendar/react": ">=6.x.x",
72-
"@hakit/core": "^5.0.8",
72+
"@hakit/core": "^5.0.9",
7373
"@mui/material": "^6.3.0",
7474
"@use-gesture/react": ">=10.x",
7575
"autolinker": ">=4.x",

packages/components/src/Cards/SidebarCard/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,16 @@ const StyledTimeCard = styled(TimeCard)<{
2929
color: var(--ha-S200-contrast);
3030
}
3131
}
32-
h4 {
32+
.time,
33+
.time-suffix {
3334
transition: var(--ha-transition-duration) var(--ha-easing);
3435
transition-property: font-size;
3536
}
3637
${(props) =>
3738
!props.open &&
3839
`
3940
padding: 0.5rem 0;
40-
h4 {
41+
.time, .time-suffix {
4142
font-size: 0.7rem;
4243
}
4344
`}

packages/components/src/Cards/TimeCard/TimeCard.stories.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,12 @@ function Template(args?: Partial<TimeCardProps>) {
1010
<ThemeControlsModal />
1111
<Row gap="1rem">
1212
<TimeCard {...args} />
13-
<TimeCard timeFormat="hh:mm:ss a" dateFormat={"MMM DD"} {...args} />
13+
<TimeCard timeFormat="hh:mm:ss A" dateFormat={"MMM DD"} {...args} />
1414
<TimeCard
1515
timeFormat={(date) => {
16-
return "WHAT? " + date.toLocaleTimeString().replace(/:/g, "-");
16+
return "Time: " + date.toLocaleTimeString().replace(/:/g, "-");
1717
}}
1818
hideDate
19-
{...args}
2019
/>
2120
</Row>
2221
<Alert
@@ -66,6 +65,15 @@ export type TimeStory = StoryObj<typeof TimeCard>;
6665
export const Docs: TimeStory = {
6766
render: Template,
6867
args: {},
68+
parameters: {
69+
docs: {
70+
source: {
71+
// language: 'graphql',
72+
dark: false,
73+
excludeDecorators: false,
74+
},
75+
},
76+
},
6977
};
7078

7179
export const WithoutDateExample: TimeStory = {

packages/components/src/Cards/TimeCard/formatter.ts renamed to packages/components/src/Cards/TimeCard/formatter.tsx

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import {
22
CustomFormatters,
33
DateParts,
44
Formatters,
5-
FormatFunction,
65
FormatterMask,
76
DatePartName,
87
FormatOptions,
8+
FormatFunction,
99
Parser,
1010
Token,
1111
} from "./types";
12+
import { AmOrPm } from "./shared";
1213

1314
const parsers: Map<string, Parser> = new Map();
1415

@@ -30,7 +31,7 @@ const intlFormattersOptions = [
3031
] satisfies Partial<Intl.DateTimeFormatOptions>[];
3132

3233
export function createDateFormatter(customFormatters: CustomFormatters): FormatFunction {
33-
return function intlFormatDate(date: Date, format: string, options?: FormatOptions): string {
34+
return function intlFormatDate(date: Date, format: string, options?: FormatOptions): React.ReactNode {
3435
const tokens = parseDate(date, options);
3536
const output = formatDate(customFormatters, format, tokens, date);
3637
return output;
@@ -135,8 +136,8 @@ const formatters: Formatters = {
135136
ddx: (parts) => `${parseInt(parts.day, 10)}${daySuffix(parseInt(parts.day))}`,
136137
dddd: (parts) => parts.weekday,
137138
ddd: (parts) => parts.weekday.slice(0, 3),
138-
A: (parts) => parts.dayPeriod,
139-
a: (parts) => parts.dayPeriod.toLowerCase(),
139+
A: (parts) => <AmOrPm className="time-suffix">{parts.dayPeriod}</AmOrPm>,
140+
a: (parts) => <AmOrPm className="time-suffix">{parts.dayPeriod.toLowerCase()}</AmOrPm>,
140141
// XXX: fix Chrome 80+ bug going over 24h
141142
HH: (parts) => ("0" + (Number(parts.lhour) % 24)).slice(-2),
142143
hh: (parts) => parts.hour,
@@ -146,14 +147,46 @@ const formatters: Formatters = {
146147

147148
const createCustomPattern = (customFormatters: CustomFormatters) => Object.keys(customFormatters).reduce((_, key) => `|${key}`, "");
148149

149-
function formatDate(customFormatters: CustomFormatters, format: string, parts: DateParts, date: Date): string {
150+
function formatDate(customFormatters: CustomFormatters, format: string, parts: DateParts, date: Date): React.ReactNode {
150151
const literalPattern = "\\[([^\\]]+)\\]|";
151152
const customPattern = createCustomPattern(customFormatters);
152153
const patternRegexp = new RegExp(`${literalPattern}${defaultPattern}${customPattern}`, "g");
153154

154155
const allFormatters = { ...formatters, ...customFormatters };
155-
// @ts-expect-error - fix later
156-
return format.replace(patternRegexp, (mask: FormatterMask, literal: string) => {
157-
return literal || allFormatters[mask](parts, date);
156+
// We'll accumulate text/JSX in an array
157+
const tokens: React.ReactNode[] = [];
158+
let lastIndex = 0;
159+
// @ts-expect-error - will fix later
160+
format.replace(patternRegexp, (mask: FormatterMask, literal: string, offset: number) => {
161+
// 'offset' is the index of this match within the full format string
162+
163+
// Push any text that occurs before this match
164+
if (offset > lastIndex) {
165+
const val = format.slice(lastIndex, offset);
166+
if (val.length > 0 && val.trim().length === 0) {
167+
// whitespace char
168+
tokens.push(<span className="whitespace">&nbsp;</span>);
169+
} else {
170+
tokens.push(val);
171+
}
172+
}
173+
lastIndex = offset + mask.length;
174+
175+
if (literal) {
176+
// If we matched [literal text], just push that as plain text
177+
tokens.push(literal);
178+
} else {
179+
// Everything else just push normally
180+
tokens.push(allFormatters[mask](parts, date));
181+
}
182+
// Return an empty string to discard the normal replace output
183+
return "";
158184
});
185+
// If there's still text remaining after the last match, push it
186+
if (lastIndex < format.length) {
187+
tokens.push(format.slice(lastIndex));
188+
}
189+
// Finally, render the HTML string inside React. We must do dangerouslySetInnerHTML
190+
// so that <div> ... </div> is recognized as HTML, not plain text:
191+
return <>{tokens}</>;
159192
}

packages/components/src/Cards/TimeCard/index.tsx

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Row, Column, fallback, CardBase, type CardBaseProps, type AvailableQuer
66
import { createDateFormatter, daySuffix } from "./formatter";
77
import { ErrorBoundary } from "react-error-boundary";
88
import { FormatFunction } from "./types";
9+
import { Time, AmOrPm } from "./shared";
910

1011
const Card = styled(CardBase)`
1112
cursor: default;
@@ -29,21 +30,6 @@ const Contents = styled.div`
2930
}
3031
`;
3132

32-
const Time = styled.h4`
33-
all: unset;
34-
font-family: var(--ha-font-family);
35-
font-size: 2rem;
36-
color: var(--ha-S200-contrast);
37-
font-weight: 400;
38-
`;
39-
const AmOrPm = styled.h4`
40-
all: unset;
41-
font-family: var(--ha-font-family);
42-
font-size: 2rem;
43-
color: var(--ha-S400-contrast);
44-
font-weight: 300;
45-
`;
46-
4733
function convertTo12Hour(time: string) {
4834
// Create a new Date object
4935
const [hour, minute] = time.split(":");
@@ -87,7 +73,7 @@ function formatDate(dateString: string): string {
8773

8874
return formattedDate;
8975
}
90-
type CustomFormatter = (date: Date, formatter: FormatFunction) => string;
76+
type CustomFormatter = (date: Date, formatter: FormatFunction) => React.ReactNode;
9177
type OmitProperties = "title" | "as" | "active" | "entity" | "service" | "serviceData" | "longPressCallback" | "modalProps";
9278
export interface TimeCardProps extends Omit<CardBaseProps<"div">, OmitProperties> {
9379
/** provide a custom entity to read the time from, if not found/provided it will update from machine time @default "sensor.time" */
@@ -116,7 +102,7 @@ export interface TimeCardProps extends Omit<CardBaseProps<"div">, OmitProperties
116102
onClick?: (entity: HassEntityWithService<"sensor">, event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
117103
}
118104

119-
const DEFAULT_TIME_FORMAT = "hh:mm a";
105+
const DEFAULT_TIME_FORMAT = "hh:mm A";
120106
const DEFAULT_DATE_FORMAT = "dddd, MMMM DD YYYY";
121107

122108
const customFormatter = createDateFormatter({});
@@ -152,6 +138,7 @@ function InternalTimeCard({
152138
const dateSensor = useEntity(dateEntity ?? "sensor.date", {
153139
returnNullIfNotFound: true,
154140
});
141+
const dateIcon = useMemo(() => icon || dateSensor?.attributes?.icon || "mdi:calendar", [icon, dateSensor]);
155142
const [formatted, amOrPm] = useMemo(() => {
156143
const parts = convertTo12Hour(timeSensor?.state ?? "00:00");
157144
const hour = parts.find((part) => part.type === "hour");
@@ -209,10 +196,12 @@ function InternalTimeCard({
209196
}, [throttleTime]);
210197

211198
useEffect(() => {
212-
if (!timeFormat && !dateFormat) return; // let home assistant trigger updates
199+
const hasEntities = timeSensor || dateSensor;
200+
const hasFormatters = timeFormat || dateFormat;
201+
if (hasEntities && !hasFormatters) return; // let home assistant trigger updates
213202
requestRef.current = requestAnimationFrame(updateClock);
214203
return () => cancelAnimationFrame(requestRef.current!);
215-
}, [updateClock, timeFormat, dateFormat]);
204+
}, [updateClock, timeFormat, dateFormat, timeSensor, dateSensor]);
216205

217206
return (
218207
<Card
@@ -236,9 +225,7 @@ function InternalTimeCard({
236225
<Column className="column" gap="0.5rem" alignItems={center ? "center" : "flex-start"} fullHeight wrap="nowrap">
237226
{(!hideIcon || !hideTime) && (
238227
<Row className="row" gap="0.5rem" alignItems="center" wrap="nowrap">
239-
{!hideIcon && (
240-
<Icon className="icon primary-icon" icon={icon || dateSensor?.attributes?.icon || "mdi:calendar"} {...(iconProps ?? {})} />
241-
)}
228+
{!hideIcon && <Icon className="icon primary-icon" icon={dateIcon} {...(iconProps ?? {})} />}
242229
{!hideTime && timeValue}
243230
</Row>
244231
)}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import styled from "@emotion/styled";
2+
3+
export const Time = styled.span`
4+
all: unset;
5+
font-family: var(--ha-font-family);
6+
font-size: 2rem;
7+
color: var(--ha-S200-contrast);
8+
font-weight: 400;
9+
display: flex;
10+
flex-direction: row;
11+
`;
12+
13+
export const AmOrPm = styled.span`
14+
all: unset;
15+
font-family: var(--ha-font-family);
16+
font-size: 2rem;
17+
color: var(--ha-S400-contrast);
18+
font-weight: 300;
19+
`;

0 commit comments

Comments
 (0)