@heliosgraphics/ui 2.0.1-alpha.9 → 2.0.1-beta.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.
@@ -49,7 +49,6 @@
49
49
  box-shadow: inset 0 0 0 1px var(--border-active);
50
50
  }
51
51
 
52
- /* Reset */
53
52
  .button .button__baseElement {
54
53
  display: inline-flex;
55
54
 
@@ -68,7 +67,6 @@
68
67
  pointer-events: none;
69
68
  }
70
69
 
71
- /* Sizes */
72
70
  .buttonSizeNormal {
73
71
  --button-badge-size: 24px;
74
72
  --button-badge-margin: 8px;
@@ -136,7 +134,6 @@
136
134
  padding-inline: 7px;
137
135
  }
138
136
 
139
- /* Loading */
140
137
  .buttonLoading,
141
138
  .buttonDisabled {
142
139
  pointer-events: none;
@@ -174,7 +171,6 @@
174
171
  right: 6px;
175
172
  }
176
173
 
177
- /* Badge */
178
174
  .button__badge {
179
175
  z-index: 2;
180
176
 
@@ -187,7 +183,6 @@
187
183
  border-radius: var(--button-badge-radius);
188
184
  }
189
185
 
190
- /* Flair */
191
186
  .button__flair {
192
187
  position: absolute;
193
188
  top: var(--button-flair-offset);
@@ -203,7 +198,6 @@
203
198
  color: var(--text);
204
199
  }
205
200
 
206
- /* Icon */
207
201
  .buttonSizeNormal .button__iconLeft,
208
202
  .buttonSizeNormal .button__iconRight {
209
203
  padding-left: 8px;
@@ -235,7 +229,6 @@
235
229
  padding-right: 5px;
236
230
  }
237
231
 
238
- /* IconRight */
239
232
  .buttonSizeNormal:not(.buttonIconOnly) .button__iconRight {
240
233
  margin-left: -12px;
241
234
  }
@@ -253,7 +246,6 @@
253
246
  opacity: 0;
254
247
  }
255
248
 
256
- /* Icon:focus */
257
249
  .buttonSizeNormal.buttonIconOnly:not(.buttonIconOnlyLoading) .button__baseElement {
258
250
  margin-left: -40px;
259
251
  width: 40px;
@@ -1,26 +1,25 @@
1
1
  .icon {
2
+ color: currentColor;
2
3
  transition: all var(--speed-sm) var(--ease-in-out-sine);
3
4
  }
4
5
 
5
6
  .icon svg {
6
7
  height: 100%;
7
8
  width: 100%;
9
+ }
8
10
 
9
- fill: currentcolor;
10
- stroke: currentcolor;
11
+ .iconPrimary {
12
+ color: var(--ui-text-primary);
11
13
  }
12
14
 
13
- .iconPrimary svg {
14
- fill: var(--ui-text-primary);
15
- stroke: var(--ui-text-primary);
15
+ .iconSecondary {
16
+ color: var(--ui-text-secondary);
16
17
  }
17
18
 
18
- .iconSecondary svg {
19
- fill: var(--ui-text-secondary);
20
- stroke: var(--ui-text-secondary);
19
+ .iconTertiary {
20
+ color: var(--ui-text-tertiary);
21
21
  }
22
22
 
23
- .iconTertiary svg {
24
- fill: var(--ui-text-tertiary);
25
- stroke: var(--ui-text-tertiary);
23
+ .iconInherit {
24
+ color: inherit;
26
25
  }
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it } from "bun:test"
2
+ import { icons } from "@heliosgraphics/icons"
3
+ import { readFileSync } from "fs"
4
+ import { dirname, join } from "path"
5
+ import { fileURLToPath } from "url"
6
+ import { getIconIdScope, getScopedIconSvg } from "./Icon.utils"
7
+
8
+ const __filename: string = fileURLToPath(import.meta.url)
9
+ const __dirname: string = dirname(__filename)
10
+
11
+ describe("Icon", () => {
12
+ it("should preserve fill-only, stroke-only, and mixed icon paint modes", () => {
13
+ expect(icons["triangle"]).toContain('fill="currentColor"')
14
+ expect(icons["triangle"]).not.toContain('stroke="currentColor"')
15
+
16
+ expect(icons["arrow-right"]).toContain('stroke="currentColor"')
17
+ expect(icons["arrow-right"]).not.toContain('fill="currentColor"')
18
+
19
+ expect(icons["tag"]).toContain('fill="currentColor"')
20
+ expect(icons["tag"]).toContain('stroke="currentColor"')
21
+ })
22
+
23
+ it("should not force fill or stroke on every svg", () => {
24
+ const iconStyles: string = readFileSync(join(__dirname, "Icon.module.css"), "utf8")
25
+
26
+ expect(iconStyles).toContain(".icon svg")
27
+ expect(iconStyles).not.toContain(".icon svg {\n\theight: 100%;\n\twidth: 100%;\n\n\tfill: currentcolor;")
28
+ expect(iconStyles).not.toContain("stroke: currentcolor;")
29
+ })
30
+
31
+ it("should scope svg definitions per icon instance", () => {
32
+ const scope: string = getIconIdScope(":r1:")
33
+ const actual: string = getScopedIconSvg(icons["layout-aside-right"], scope)
34
+
35
+ expect(actual).toContain(`id="${scope}-a"`)
36
+ expect(actual).toContain(`mask="url(#${scope}-a)"`)
37
+ expect(actual).not.toContain('id="a"')
38
+ expect(actual).not.toContain("url(#a)")
39
+ })
40
+ })
@@ -1,10 +1,15 @@
1
+ "use client"
2
+
1
3
  import { getClasses } from "@heliosgraphics/utils"
2
4
  import { icons } from "@heliosgraphics/icons"
3
5
  import styles from "./Icon.module.css"
4
- import type { FC } from "react"
6
+ import { useId, type FC } from "react"
7
+ import { getIconIdScope, getScopedIconSvg } from "./Icon.utils"
5
8
  import type { IconProps } from "./Icon.types"
6
9
 
7
10
  export const Icon: FC<IconProps> = ({ icon, className, emphasis, size }) => {
11
+ const iconId: string = getIconIdScope(useId())
12
+ const iconSvg: string = getScopedIconSvg(icons[icon] || "", iconId)
8
13
  const iconSizeStyle = {
9
14
  height: size + "px",
10
15
  minHeight: size + "px",
@@ -16,6 +21,7 @@ export const Icon: FC<IconProps> = ({ icon, className, emphasis, size }) => {
16
21
  [styles.iconPrimary]: emphasis === "primary",
17
22
  [styles.iconSecondary]: emphasis === "secondary",
18
23
  [styles.iconTertiary]: emphasis === "tertiary",
24
+ [styles.iconInherit]: emphasis === "inherit",
19
25
  })
20
26
 
21
27
  return (
@@ -25,7 +31,7 @@ export const Icon: FC<IconProps> = ({ icon, className, emphasis, size }) => {
25
31
  aria-hidden={true}
26
32
  data-ui-component="Icon"
27
33
  data-ui-icon={icon}
28
- dangerouslySetInnerHTML={{ __html: icons[icon] || "" }}
34
+ dangerouslySetInnerHTML={{ __html: iconSvg }}
29
35
  />
30
36
  )
31
37
  }
@@ -0,0 +1,11 @@
1
+ const HREF_REFERENCE_REGEX = /\b(href|xlink:href)="#([^"]+)"/g
2
+ const ID_ATTRIBUTE_REGEX = /\bid="([^"]+)"/g
3
+ const URL_REFERENCE_REGEX = /url\(#([^\)]+)\)/g
4
+
5
+ export const getIconIdScope = (id: string): string => `icon-${id.replace(/[^a-zA-Z0-9_-]/g, "")}`
6
+
7
+ export const getScopedIconSvg = (svg: string, scope: string): string =>
8
+ svg
9
+ .replace(ID_ATTRIBUTE_REGEX, `id="${scope}-$1"`)
10
+ .replace(URL_REFERENCE_REGEX, `url(#${scope}-$1)`)
11
+ .replace(HREF_REFERENCE_REGEX, `$1="#${scope}-$2"`)
@@ -1,8 +1,8 @@
1
1
  "use client"
2
2
 
3
3
  import type { FC } from "react"
4
- import { Button } from "../../../../../Button/Button"
5
- import { ButtonGroup } from "../../../../../ButtonGroup/ButtonGroup"
4
+ import { Button } from "../../../../../Button"
5
+ import { ButtonGroup } from "../../../../../ButtonGroup"
6
6
  import { useLayoutContext } from "../../../../../../hooks/useLayoutContext"
7
7
  import type { LayoutAsideToggleProps } from "./LayoutAsideToggle.types"
8
8
 
@@ -67,7 +67,6 @@
67
67
  line-height: normal;
68
68
  }
69
69
 
70
- /* Element: Blockquote */
71
70
  .markdown blockquote {
72
71
  position: relative;
73
72
 
@@ -110,7 +109,6 @@
110
109
  line-height: inherit;
111
110
  }
112
111
 
113
- /* Element: Link */
114
112
  .markdown a {
115
113
  color: oklch(var(--ui-text-soft-l) var(--ui-chroma) var(--ui-blue));
116
114
 
@@ -118,7 +116,6 @@
118
116
  text-decoration: underline;
119
117
  }
120
118
 
121
- /* Element: List */
122
119
  .markdown ul li {
123
120
  list-style-type: disc;
124
121
  }
@@ -140,13 +137,11 @@
140
137
  list-style-type: circle;
141
138
  }
142
139
 
143
- /* Element: Separator */
144
140
  .markdown hr {
145
141
  border: 0;
146
142
  border-top: 1px solid var(--ui-border-secondary);
147
143
  }
148
144
 
149
- /* Element: Table */
150
145
  .markdown table {
151
146
  max-width: 100%;
152
147
 
@@ -181,7 +176,6 @@
181
176
  border-radius: var(--radius-md);
182
177
  }
183
178
 
184
- /* Element: Headings */
185
179
  .markdown h1 {
186
180
  font-size: var(--font-size-h1);
187
181
  line-height: var(--line-height-h1);
@@ -222,24 +216,20 @@
222
216
  line-height: var(--line-height-p);
223
217
  }
224
218
 
225
- /* Element: strong, bold: 700 */
226
219
  .markdown b,
227
220
  .markdown strong {
228
221
  font-weight: var(--font-weight-bold);
229
222
  }
230
223
 
231
- /* Element: Delete */
232
224
  .markdown del {
233
225
  text-decoration: line-through;
234
226
  }
235
227
 
236
- /* Element: Italic */
237
228
  .markdown i,
238
229
  .markdown em {
239
230
  font-style: italic;
240
231
  }
241
232
 
242
- /* Modifier: Code */
243
233
  .markdown *:not(pre) code {
244
234
  display: inline-block;
245
235
  padding: 0 4px;
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from "bun:test"
2
+ import type { HeliosFixedThemeType } from "../../types/themes"
3
+ import { code } from "./Setup.utils"
4
+
5
+ describe("Setup", () => {
6
+ it("should execute when serialized into an inline script", () => {
7
+ const rootDataset: Record<string, string> = {}
8
+ const localStorageValues = new Map<string, string>()
9
+ const listeners: Record<string, (event: { matches: boolean }) => void> = {}
10
+
11
+ globalThis.__theme = "light" as HeliosFixedThemeType
12
+ globalThis.__onThemeChange = undefined
13
+ globalThis.__setPreferredTheme = (): void => {}
14
+ globalThis.document = {
15
+ documentElement: {
16
+ dataset: rootDataset,
17
+ },
18
+ } as Document
19
+ globalThis.localStorage = {
20
+ getItem: (key: string): string | null => localStorageValues.get(key) ?? null,
21
+ setItem: (key: string, value: string): void => {
22
+ localStorageValues.set(key, value)
23
+ },
24
+ } as Storage
25
+ globalThis.matchMedia = ((query: string): MediaQueryList => {
26
+ expect(query).toBe("(prefers-color-scheme: dark)")
27
+ return {
28
+ matches: true,
29
+ addEventListener: (type: string, listener: (event: { matches: boolean }) => void): void => {
30
+ listeners[type] = listener
31
+ },
32
+ } as MediaQueryList
33
+ }) as typeof globalThis.matchMedia
34
+
35
+ new Function(`return (${code.toString()})("system", undefined)`)()
36
+
37
+ expect(globalThis.__theme).toBe("dark")
38
+ expect(rootDataset["theme"]).toBe("dark")
39
+ expect(typeof listeners["change"]).toBe("function")
40
+
41
+ const changeListener = listeners["change"]
42
+ expect(changeListener).toBeDefined()
43
+ if (!changeListener) {
44
+ throw new Error("expected change listener to be registered")
45
+ }
46
+
47
+ changeListener({ matches: false })
48
+
49
+ expect(globalThis.__theme).toBe("light")
50
+ expect(rootDataset["theme"]).toBe("light")
51
+ expect(localStorageValues.get("theme")).toBe("light")
52
+ })
53
+ })
@@ -0,0 +1,55 @@
1
+ import type { HeliosFixedThemeType, HeliosThemeType } from "../../types/themes"
2
+
3
+ export const code = (theme: HeliosThemeType = "system", fixedTheme?: HeliosFixedThemeType): void => {
4
+ globalThis.__onThemeChange = function (_theme: HeliosFixedThemeType): void {}
5
+
6
+ /* eslint-disable-next-line unicorn/consistent-function-scoping */
7
+ const setTheme = (newTheme: HeliosFixedThemeType): void => {
8
+ globalThis.__theme = newTheme
9
+ document.documentElement.dataset["theme"] = newTheme
10
+ globalThis.__onThemeChange?.(newTheme)
11
+ }
12
+
13
+ /* eslint-disable-next-line unicorn/consistent-function-scoping */
14
+ const handleDarkModeChange = (event: MediaQueryListEvent): void => {
15
+ globalThis.__setPreferredTheme(event.matches ? "dark" : "light")
16
+ }
17
+
18
+ const isLocked: boolean = fixedTheme !== undefined || theme !== "system"
19
+
20
+ if (fixedTheme !== undefined && theme !== "system") {
21
+ console.warn("Setup: Both theme and fixedTheme props are set. fixedTheme takes precedence, theme prop is ignored.")
22
+ }
23
+
24
+ if (isLocked) {
25
+ const lockedTheme: HeliosFixedThemeType = fixedTheme ?? (theme as HeliosFixedThemeType)
26
+ globalThis.__setPreferredTheme = function (_newTheme: HeliosFixedThemeType): void {}
27
+ setTheme(lockedTheme)
28
+ return
29
+ }
30
+
31
+ let preferredTheme: HeliosFixedThemeType | undefined
32
+ const darkQuery: MediaQueryList = globalThis.matchMedia("(prefers-color-scheme: dark)")
33
+
34
+ try {
35
+ const storedTheme: string | null = globalThis.localStorage.getItem("theme")
36
+ if (storedTheme === "dark" || storedTheme === "light") {
37
+ preferredTheme = storedTheme
38
+ }
39
+ } catch (error: unknown) {
40
+ console.error("failed to read theme from localStorage:", error)
41
+ }
42
+
43
+ globalThis.__setPreferredTheme = function (newTheme: HeliosFixedThemeType): void {
44
+ try {
45
+ setTheme(newTheme)
46
+ globalThis.localStorage.setItem("theme", newTheme)
47
+ } catch (error: unknown) {
48
+ console.error("failed to set theme:", error)
49
+ }
50
+ }
51
+
52
+ darkQuery.addEventListener("change", handleDarkModeChange)
53
+
54
+ setTheme(preferredTheme ?? (darkQuery.matches ? "dark" : "light"))
55
+ }
@@ -1,7 +1,4 @@
1
1
  @layer helios {
2
- /* TODO sync with css */
3
-
4
- /* background */
5
2
  .ui-bg,
6
3
  .ui-bg-primary {
7
4
  background-color: var(--ui-bg-primary);
@@ -20,7 +17,6 @@
20
17
  backdrop-filter: blur(3px);
21
18
  }
22
19
 
23
- /* radius utilities */
24
20
  .radius-none {
25
21
  border-radius: 0;
26
22
  }
@@ -53,7 +49,6 @@
53
49
  border-radius: 9999px;
54
50
  }
55
51
 
56
- /* scrollbar */
57
52
  .ui-scrollbar::-webkit-scrollbar {
58
53
  height: 8px;
59
54
  width: 8px;
@@ -84,7 +79,6 @@
84
79
  background-color: var(--ui-bg-soft-disabled-gray);
85
80
  }
86
81
 
87
- /* elevation */
88
82
  .elevation-sm {
89
83
  box-shadow: var(--ui-elevation-sm);
90
84
  }
@@ -97,7 +91,6 @@
97
91
  box-shadow: var(--ui-elevation-lg);
98
92
  }
99
93
 
100
- /* debug */
101
94
  .debug * {
102
95
  box-shadow: inset 0 0 0 1px var(--ui-bg-red);
103
96
  }
@@ -3,7 +3,6 @@
3
3
 
4
4
  background-color: var(--bg);
5
5
  border: 1px solid var(--border);
6
- border-radius: var(--radius-md);
7
6
  color: var(--text);
8
7
  overflow: hidden;
9
8
  transition: all var(--speed-sm) var(--ease-in-out-sine);
@@ -34,8 +34,8 @@ export const Tile: FC<TileProps> = ({
34
34
  const tileColorClasses: Array<string> = getColorClasses(resolvedColor, appearance)
35
35
  const tileClasses = getClasses(styles.tile, ...tileColorClasses, {
36
36
  [styles.tileLarge]: isLarge,
37
- ["radius-normal"]: isRounded,
38
- ["radius-max"]: isRound,
37
+ ["radius-md"]: isRounded,
38
+ ["radius-100"]: isRound,
39
39
  })
40
40
 
41
41
  return (
@@ -32,9 +32,9 @@ const RESULT_ITEMS: Array<ResultItem> = [
32
32
  onClick: () => null,
33
33
  description: "Nulla ultricies ultrices mauris, sed posuere justo ultrices in.",
34
34
  },
35
- { name: "Random Item", icon: "person" },
35
+ { name: "Random Item", icon: "user" },
36
36
  { name: "Active Item", icon: "bell", isActive: true },
37
- { name: "Lorem Ipsum Dolor Sit Ameta", icon: "x-x" },
37
+ { name: "Lorem Ipsum Dolor Sit Ameta", icon: "brand-x" },
38
38
  ]
39
39
 
40
40
  const MARKDOWN: string = `# Donec vestibulum
@@ -41,7 +41,6 @@ const LayoutProvider: FC<LayoutProviderProps> = ({ children, breakpoint = 960 })
41
41
  }
42
42
  }
43
43
 
44
- // Set initial state
45
44
  handleChange(mql)
46
45
 
47
46
  mql.addEventListener("change", handleChange)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heliosgraphics/ui",
3
- "version": "2.0.1-alpha.9",
3
+ "version": "2.0.1-beta.0",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "*.css",
@@ -41,11 +41,11 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@heliosgraphics/css": "^1.0.0-alpha.10",
44
- "@heliosgraphics/icons": "^1.0.0-alpha.14",
45
- "@heliosgraphics/utils": "^6.0.0-alpha.16",
46
- "marked": "^17.0.5",
47
- "marked-linkify-it": "^3.1.14",
48
- "marked-xhtml": "^1.0.14",
44
+ "@heliosgraphics/icons": "^1.0.0-beta.0",
45
+ "@heliosgraphics/utils": "^6.0.0-alpha.17",
46
+ "marked": "^18.0.3",
47
+ "marked-linkify-it": "^3.1.15",
48
+ "marked-xhtml": "^1.0.15",
49
49
  "react-plock": "^3.6.1"
50
50
  },
51
51
  "devDependencies": {
@@ -54,10 +54,10 @@
54
54
  "esbuild": "^0.28.0",
55
55
  "esbuild-css-modules-plugin": "^3.1.5",
56
56
  "glob": "^13.0.6",
57
- "jsdom": "^29.0.1",
58
- "next": "^16.2.2",
59
- "prettier": "^3.8.1",
60
- "typescript": "^6.0.2"
57
+ "jsdom": "^29.1.1",
58
+ "next": "^16.2.6",
59
+ "prettier": "^3.8.3",
60
+ "typescript": "^6.0.3"
61
61
  },
62
62
  "peerDependencies": {
63
63
  "@types/react": "^19",