mirror of
https://github.com/kennethnym/aris.git
synced 2026-03-20 17:11:17 +00:00
Compare commits
4 Commits
c78ad25f0d
...
feat/calda
| Author | SHA1 | Date | |
|---|---|---|---|
| 1129d52906 | |||
| 45fa539d3e | |||
| b4ad910a14 | |||
| d3452dd452 |
@@ -18,6 +18,7 @@
|
|||||||
"@expo-google-fonts/inter": "^0.4.2",
|
"@expo-google-fonts/inter": "^0.4.2",
|
||||||
"@expo-google-fonts/source-serif-4": "^0.4.1",
|
"@expo-google-fonts/source-serif-4": "^0.4.1",
|
||||||
"@expo/vector-icons": "^15.0.3",
|
"@expo/vector-icons": "^15.0.3",
|
||||||
|
"@json-render/react-native": "^0.13.0",
|
||||||
"@react-navigation/bottom-tabs": "^7.4.0",
|
"@react-navigation/bottom-tabs": "^7.4.0",
|
||||||
"@react-navigation/elements": "^2.6.3",
|
"@react-navigation/elements": "^2.6.3",
|
||||||
"@react-navigation/native": "^7.1.8",
|
"@react-navigation/native": "^7.1.8",
|
||||||
@@ -45,7 +46,8 @@
|
|||||||
"react-native-svg": "15.12.1",
|
"react-native-svg": "15.12.1",
|
||||||
"react-native-web": "~0.21.0",
|
"react-native-web": "~0.21.0",
|
||||||
"react-native-worklets": "0.5.1",
|
"react-native-worklets": "0.5.1",
|
||||||
"twrnc": "^4.16.0"
|
"twrnc": "^4.16.0",
|
||||||
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "~19.1.0",
|
"@types/react": "~19.1.0",
|
||||||
|
|||||||
68
apps/aelis-client/src/json-render/catalog.ts
Normal file
68
apps/aelis-client/src/json-render/catalog.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { defineCatalog } from "@json-render/core"
|
||||||
|
import { schema } from "@json-render/react-native/schema"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
export const catalog = defineCatalog(schema, {
|
||||||
|
components: {
|
||||||
|
View: {
|
||||||
|
props: z.object({
|
||||||
|
style: z.string().nullable(),
|
||||||
|
}),
|
||||||
|
slots: ["default"],
|
||||||
|
description:
|
||||||
|
"Generic layout container. The style prop accepts a twrnc class string (e.g. 'flex-row gap-2 p-4 items-center').",
|
||||||
|
example: { style: "flex-row gap-2 p-4" },
|
||||||
|
},
|
||||||
|
Button: {
|
||||||
|
props: z.object({
|
||||||
|
label: z.string(),
|
||||||
|
leadingIcon: z.string().nullable(),
|
||||||
|
trailingIcon: z.string().nullable(),
|
||||||
|
}),
|
||||||
|
events: ["press"],
|
||||||
|
slots: [],
|
||||||
|
description:
|
||||||
|
"Pressable button with a label and optional Feather icons. Icon values are Feather icon names (e.g. 'plus', 'arrow-right'). Bind on.press to trigger an action.",
|
||||||
|
example: { label: "Add item", leadingIcon: "plus", trailingIcon: null },
|
||||||
|
},
|
||||||
|
FeedCard: {
|
||||||
|
props: z.object({
|
||||||
|
style: z.string().nullable(),
|
||||||
|
}),
|
||||||
|
slots: ["default"],
|
||||||
|
description: "Bordered card container for feed content. The style prop accepts a twrnc class string.",
|
||||||
|
example: { style: "p-4 gap-2" },
|
||||||
|
},
|
||||||
|
SansSerifText: {
|
||||||
|
props: z.object({
|
||||||
|
text: z.string(),
|
||||||
|
style: z.string().nullable(),
|
||||||
|
}),
|
||||||
|
slots: [],
|
||||||
|
description:
|
||||||
|
"Sans-serif text (Inter font). The style prop accepts a twrnc class string for size, weight, color, etc.",
|
||||||
|
example: { text: "Hello world", style: "text-base font-medium" },
|
||||||
|
},
|
||||||
|
SerifText: {
|
||||||
|
props: z.object({
|
||||||
|
text: z.string(),
|
||||||
|
style: z.string().nullable(),
|
||||||
|
}),
|
||||||
|
slots: [],
|
||||||
|
description:
|
||||||
|
"Serif text (Source Serif 4 font). The style prop accepts a twrnc class string for size, color, etc.",
|
||||||
|
example: { text: "Heading", style: "text-xl" },
|
||||||
|
},
|
||||||
|
MonospaceText: {
|
||||||
|
props: z.object({
|
||||||
|
text: z.string(),
|
||||||
|
style: z.string().nullable(),
|
||||||
|
}),
|
||||||
|
slots: [],
|
||||||
|
description:
|
||||||
|
"Monospace text (Menlo font). The style prop accepts a twrnc class string for size, color, etc.",
|
||||||
|
example: { text: "const x = 42", style: "text-sm" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {},
|
||||||
|
})
|
||||||
2
apps/aelis-client/src/json-render/index.ts
Normal file
2
apps/aelis-client/src/json-render/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { catalog } from "./catalog"
|
||||||
|
export { registry } from "./registry"
|
||||||
43
apps/aelis-client/src/json-render/registry.tsx
Normal file
43
apps/aelis-client/src/json-render/registry.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import Feather from "@expo/vector-icons/Feather"
|
||||||
|
import { defineRegistry } from "@json-render/react-native"
|
||||||
|
import { View } from "react-native"
|
||||||
|
import tw from "twrnc"
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { FeedCard } from "@/components/ui/feed-card"
|
||||||
|
import { MonospaceText } from "@/components/ui/monospace-text"
|
||||||
|
import { SansSerifText } from "@/components/ui/sans-serif-text"
|
||||||
|
import { SerifText } from "@/components/ui/serif-text"
|
||||||
|
|
||||||
|
import { catalog } from "./catalog"
|
||||||
|
|
||||||
|
function featherIcon(name: string | null | undefined) {
|
||||||
|
if (!name) return undefined
|
||||||
|
return <Feather name={name as React.ComponentProps<typeof Feather>["name"]} size={18} color="#e7e5e4" />
|
||||||
|
}
|
||||||
|
|
||||||
|
export const { registry } = defineRegistry(catalog, {
|
||||||
|
components: {
|
||||||
|
View: ({ props, children }) => <View style={props.style ? tw`${props.style}` : undefined}>{children}</View>,
|
||||||
|
Button: ({ props, emit }) => (
|
||||||
|
<Button
|
||||||
|
label={props.label}
|
||||||
|
leadingIcon={featherIcon(props.leadingIcon)}
|
||||||
|
trailingIcon={featherIcon(props.trailingIcon)}
|
||||||
|
onPress={() => emit("press")}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
FeedCard: ({ props, children }) => (
|
||||||
|
<FeedCard style={props.style ? tw`${props.style}` : undefined}>{children}</FeedCard>
|
||||||
|
),
|
||||||
|
SansSerifText: ({ props }) => (
|
||||||
|
<SansSerifText style={props.style ? tw`${props.style}` : undefined}>{props.text}</SansSerifText>
|
||||||
|
),
|
||||||
|
SerifText: ({ props }) => (
|
||||||
|
<SerifText style={props.style ? tw`${props.style}` : undefined}>{props.text}</SerifText>
|
||||||
|
),
|
||||||
|
MonospaceText: ({ props }) => (
|
||||||
|
<MonospaceText style={props.style ? tw`${props.style}` : undefined}>{props.text}</MonospaceText>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
})
|
||||||
21
bun.lock
21
bun.lock
@@ -6,7 +6,7 @@
|
|||||||
"name": "aelis",
|
"name": "aelis",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@json-render/core": "^0.12.1",
|
"@json-render/core": "^0.12.1",
|
||||||
"@nym.sh/jrx": "^0.1.0",
|
"@nym.sh/jrx": "^0.2.0",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"oxfmt": "^0.24.0",
|
"oxfmt": "^0.24.0",
|
||||||
"oxlint": "^1.39.0",
|
"oxlint": "^1.39.0",
|
||||||
@@ -42,6 +42,7 @@
|
|||||||
"@expo-google-fonts/inter": "^0.4.2",
|
"@expo-google-fonts/inter": "^0.4.2",
|
||||||
"@expo-google-fonts/source-serif-4": "^0.4.1",
|
"@expo-google-fonts/source-serif-4": "^0.4.1",
|
||||||
"@expo/vector-icons": "^15.0.3",
|
"@expo/vector-icons": "^15.0.3",
|
||||||
|
"@json-render/react-native": "^0.13.0",
|
||||||
"@react-navigation/bottom-tabs": "^7.4.0",
|
"@react-navigation/bottom-tabs": "^7.4.0",
|
||||||
"@react-navigation/elements": "^2.6.3",
|
"@react-navigation/elements": "^2.6.3",
|
||||||
"@react-navigation/native": "^7.1.8",
|
"@react-navigation/native": "^7.1.8",
|
||||||
@@ -70,6 +71,7 @@
|
|||||||
"react-native-web": "~0.21.0",
|
"react-native-web": "~0.21.0",
|
||||||
"react-native-worklets": "0.5.1",
|
"react-native-worklets": "0.5.1",
|
||||||
"twrnc": "^4.16.0",
|
"twrnc": "^4.16.0",
|
||||||
|
"zod": "^4.3.6",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "~19.1.0",
|
"@types/react": "~19.1.0",
|
||||||
@@ -108,6 +110,14 @@
|
|||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"packages/aelis-components": {
|
||||||
|
"name": "@aelis/components",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@json-render/core": "*",
|
||||||
|
"@nym.sh/jrx": "*",
|
||||||
|
},
|
||||||
|
},
|
||||||
"packages/aelis-core": {
|
"packages/aelis-core": {
|
||||||
"name": "@aelis/core",
|
"name": "@aelis/core",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
@@ -142,6 +152,7 @@
|
|||||||
"name": "@aelis/source-caldav",
|
"name": "@aelis/source-caldav",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@aelis/components": "workspace:*",
|
||||||
"@aelis/core": "workspace:*",
|
"@aelis/core": "workspace:*",
|
||||||
"ical.js": "^2.1.0",
|
"ical.js": "^2.1.0",
|
||||||
"tsdav": "^2.1.7",
|
"tsdav": "^2.1.7",
|
||||||
@@ -187,6 +198,8 @@
|
|||||||
|
|
||||||
"@aelis/backend": ["@aelis/backend@workspace:apps/aelis-backend"],
|
"@aelis/backend": ["@aelis/backend@workspace:apps/aelis-backend"],
|
||||||
|
|
||||||
|
"@aelis/components": ["@aelis/components@workspace:packages/aelis-components"],
|
||||||
|
|
||||||
"@aelis/core": ["@aelis/core@workspace:packages/aelis-core"],
|
"@aelis/core": ["@aelis/core@workspace:packages/aelis-core"],
|
||||||
|
|
||||||
"@aelis/data-source-weatherkit": ["@aelis/data-source-weatherkit@workspace:packages/aelis-data-source-weatherkit"],
|
"@aelis/data-source-weatherkit": ["@aelis/data-source-weatherkit@workspace:packages/aelis-data-source-weatherkit"],
|
||||||
@@ -657,6 +670,8 @@
|
|||||||
|
|
||||||
"@json-render/core": ["@json-render/core@0.12.1", "", { "dependencies": { "zod": "^4.3.6" } }, "sha512-1tV/481GPHmIRd6lXfWcTaIslQusmDg5lzcSBzWLkSXjF9sjjyOQL090in7uHT4tOMWkdmlEJOW5H9C72PsUEQ=="],
|
"@json-render/core": ["@json-render/core@0.12.1", "", { "dependencies": { "zod": "^4.3.6" } }, "sha512-1tV/481GPHmIRd6lXfWcTaIslQusmDg5lzcSBzWLkSXjF9sjjyOQL090in7uHT4tOMWkdmlEJOW5H9C72PsUEQ=="],
|
||||||
|
|
||||||
|
"@json-render/react-native": ["@json-render/react-native@0.13.0", "", { "dependencies": { "@json-render/core": "0.13.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-native": ">=0.71.0", "zod": "^4.0.0" } }, "sha512-uUrK28xPb7LuyYsi9cTnvrnXnVBG0OwU5Up35aaXwcWMLrfqxJ7oWfF97HlDvZIckQtm0VEngAXhHMW97qBEkg=="],
|
||||||
|
|
||||||
"@mjackson/node-fetch-server": ["@mjackson/node-fetch-server@0.2.0", "", {}, "sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng=="],
|
"@mjackson/node-fetch-server": ["@mjackson/node-fetch-server@0.2.0", "", {}, "sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng=="],
|
||||||
|
|
||||||
"@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.4.6", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g=="],
|
"@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.4.6", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g=="],
|
||||||
@@ -677,7 +692,7 @@
|
|||||||
|
|
||||||
"@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="],
|
"@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="],
|
||||||
|
|
||||||
"@nym.sh/jrx": ["@nym.sh/jrx@0.1.0", "", { "peerDependencies": { "@json-render/core": ">=0.10.0" } }, "sha512-mu6fkAP/TI9FuP8A4WMCrcucpUtWF5xBTcETnrjOtvEED9i+7sQKuoOyhJeF6QaSuUkAA/8t3Xx3kYUjcAPFbw=="],
|
"@nym.sh/jrx": ["@nym.sh/jrx@0.2.0", "", { "peerDependencies": { "@json-render/core": ">=0.10.0" } }, "sha512-jd7Z1Q6T21366MtSUnwCFiu6Yl1AdNc9s5m6HxeUg265P+0enZCiyyxOuHsFwvpUcSEs/2DVBsqfMptdca44lA=="],
|
||||||
|
|
||||||
"@oclif/core": ["@oclif/core@4.8.4", "", { "dependencies": { "ansi-escapes": "^4.3.2", "ansis": "^3.17.0", "clean-stack": "^3.0.1", "cli-spinners": "^2.9.2", "debug": "^4.4.3", "ejs": "^3.1.10", "get-package-type": "^0.1.0", "indent-string": "^4.0.0", "is-wsl": "^2.2.0", "lilconfig": "^3.1.3", "minimatch": "^10.2.4", "semver": "^7.7.3", "string-width": "^4.2.3", "supports-color": "^8", "tinyglobby": "^0.2.14", "widest-line": "^3.1.0", "wordwrap": "^1.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-UTAqwXJJyRvLBvosL+1uPZYSpr8lEHgUb/EVGbPXo5WZqUIBHfJ0sR2bkBEsrj00/ar4IegKxx4YK0wn2c8SQg=="],
|
"@oclif/core": ["@oclif/core@4.8.4", "", { "dependencies": { "ansi-escapes": "^4.3.2", "ansis": "^3.17.0", "clean-stack": "^3.0.1", "cli-spinners": "^2.9.2", "debug": "^4.4.3", "ejs": "^3.1.10", "get-package-type": "^0.1.0", "indent-string": "^4.0.0", "is-wsl": "^2.2.0", "lilconfig": "^3.1.3", "minimatch": "^10.2.4", "semver": "^7.7.3", "string-width": "^4.2.3", "supports-color": "^8", "tinyglobby": "^0.2.14", "widest-line": "^3.1.0", "wordwrap": "^1.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-UTAqwXJJyRvLBvosL+1uPZYSpr8lEHgUb/EVGbPXo5WZqUIBHfJ0sR2bkBEsrj00/ar4IegKxx4YK0wn2c8SQg=="],
|
||||||
|
|
||||||
@@ -3457,6 +3472,8 @@
|
|||||||
|
|
||||||
"@jest/transform/write-file-atomic": ["write-file-atomic@4.0.2", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" } }, "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg=="],
|
"@jest/transform/write-file-atomic": ["write-file-atomic@4.0.2", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" } }, "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg=="],
|
||||||
|
|
||||||
|
"@json-render/react-native/@json-render/core": ["@json-render/core@0.13.0", "", { "dependencies": { "zod": "^4.3.6" } }, "sha512-CXmCsc8BHDRq45ScVd+qgvjTbwZHPVpVD05WnTqgDxqfY3LGXu5vxaQRSwYoEodg/DGcZq/4HSj4ipVvrzy3qQ=="],
|
||||||
|
|
||||||
"@mrleebo/prisma-ast/lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
|
"@mrleebo/prisma-ast/lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
|
||||||
|
|
||||||
"@oclif/core/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
|
"@oclif/core/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@json-render/core": "^0.12.1",
|
"@json-render/core": "^0.12.1",
|
||||||
"@nym.sh/jrx": "^0.1.0",
|
"@nym.sh/jrx": "^0.2.0",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"oxfmt": "^0.24.0",
|
"oxfmt": "^0.24.0",
|
||||||
"oxlint": "^1.39.0"
|
"oxlint": "^1.39.0"
|
||||||
|
|||||||
14
packages/aelis-components/package.json
Normal file
14
packages/aelis-components/package.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "@aelis/components",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"types": "src/index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"test": "bun test ./src"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@json-render/core": "*",
|
||||||
|
"@nym.sh/jrx": "*"
|
||||||
|
}
|
||||||
|
}
|
||||||
15
packages/aelis-components/src/button.ts
Normal file
15
packages/aelis-components/src/button.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import type { JrxNode } from "@nym.sh/jrx"
|
||||||
|
|
||||||
|
import { jsx } from "@nym.sh/jrx/jsx-runtime"
|
||||||
|
|
||||||
|
export type ButtonProps = {
|
||||||
|
label: string
|
||||||
|
leadingIcon?: string
|
||||||
|
trailingIcon?: string
|
||||||
|
style?: string
|
||||||
|
children?: JrxNode | JrxNode[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Button(props: ButtonProps): JrxNode {
|
||||||
|
return jsx("Button", props)
|
||||||
|
}
|
||||||
155
packages/aelis-components/src/components.test.tsx
Normal file
155
packages/aelis-components/src/components.test.tsx
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
/** @jsxImportSource @nym.sh/jrx */
|
||||||
|
|
||||||
|
import { render } from "@nym.sh/jrx"
|
||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
|
||||||
|
import { Button } from "./button.ts"
|
||||||
|
import { FeedCard } from "./feed-card.ts"
|
||||||
|
import { MonospaceText } from "./monospace-text.ts"
|
||||||
|
import { SansSerifText } from "./sans-serif-text.ts"
|
||||||
|
import { SerifText } from "./serif-text.ts"
|
||||||
|
|
||||||
|
describe("Button", () => {
|
||||||
|
test("renders with label", () => {
|
||||||
|
const spec = render(<Button label="Press me" />)
|
||||||
|
|
||||||
|
expect(spec.root).toStartWith("button-")
|
||||||
|
const root = spec.elements[spec.root]!
|
||||||
|
expect(root.type).toBe("Button")
|
||||||
|
expect(root.props).toEqual({ label: "Press me" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("renders with icon props", () => {
|
||||||
|
const spec = render(<Button label="Add" leadingIcon="plus" trailingIcon="arrow-right" />)
|
||||||
|
|
||||||
|
const root = spec.elements[spec.root]!
|
||||||
|
expect(root.type).toBe("Button")
|
||||||
|
expect(root.props).toEqual({
|
||||||
|
label: "Add",
|
||||||
|
leadingIcon: "plus",
|
||||||
|
trailingIcon: "arrow-right",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("passes style as string prop", () => {
|
||||||
|
const spec = render(<Button label="Go" style="px-4 py-2" />)
|
||||||
|
|
||||||
|
const root = spec.elements[spec.root]!
|
||||||
|
expect(root.props.style).toBe("px-4 py-2")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("FeedCard", () => {
|
||||||
|
test("renders as container", () => {
|
||||||
|
const spec = render(<FeedCard />)
|
||||||
|
|
||||||
|
expect(spec.root).toStartWith("feedcard-")
|
||||||
|
const root = spec.elements[spec.root]!
|
||||||
|
expect(root.type).toBe("FeedCard")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("renders with a single child", () => {
|
||||||
|
const spec = render(
|
||||||
|
<FeedCard>
|
||||||
|
<SansSerifText content="Only child" />
|
||||||
|
</FeedCard>,
|
||||||
|
)
|
||||||
|
|
||||||
|
const root = spec.elements[spec.root]!
|
||||||
|
expect(root.children).toHaveLength(1)
|
||||||
|
const child = spec.elements[root.children![0]!]!
|
||||||
|
expect(child.type).toBe("SansSerifText")
|
||||||
|
expect(child.props).toEqual({ content: "Only child" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("passes style as string prop", () => {
|
||||||
|
const spec = render(<FeedCard style="p-4 border rounded-lg" />)
|
||||||
|
|
||||||
|
const root = spec.elements[spec.root]!
|
||||||
|
expect(root.props.style).toBe("p-4 border rounded-lg")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("SansSerifText", () => {
|
||||||
|
test("renders with content prop", () => {
|
||||||
|
const spec = render(<SansSerifText content="Hello" />)
|
||||||
|
|
||||||
|
expect(spec.root).toStartWith("sansseriftext-")
|
||||||
|
const root = spec.elements[spec.root]!
|
||||||
|
expect(root.type).toBe("SansSerifText")
|
||||||
|
expect(root.props).toEqual({ content: "Hello" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("passes style as string prop", () => {
|
||||||
|
const spec = render(<SansSerifText content="Hello" style="text-sm text-stone-500" />)
|
||||||
|
|
||||||
|
const root = spec.elements[spec.root]!
|
||||||
|
expect(root.props.style).toBe("text-sm text-stone-500")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("SerifText", () => {
|
||||||
|
test("renders with content prop", () => {
|
||||||
|
const spec = render(<SerifText content="Title" />)
|
||||||
|
|
||||||
|
expect(spec.root).toStartWith("seriftext-")
|
||||||
|
const root = spec.elements[spec.root]!
|
||||||
|
expect(root.type).toBe("SerifText")
|
||||||
|
expect(root.props).toEqual({ content: "Title" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("passes style as string prop", () => {
|
||||||
|
const spec = render(<SerifText content="Title" style="text-xl" />)
|
||||||
|
|
||||||
|
const root = spec.elements[spec.root]!
|
||||||
|
expect(root.props.style).toBe("text-xl")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("MonospaceText", () => {
|
||||||
|
test("renders with content prop", () => {
|
||||||
|
const spec = render(<MonospaceText content="code()" />)
|
||||||
|
|
||||||
|
expect(spec.root).toStartWith("monospacetext-")
|
||||||
|
const root = spec.elements[spec.root]!
|
||||||
|
expect(root.type).toBe("MonospaceText")
|
||||||
|
expect(root.props).toEqual({ content: "code()" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("passes style as string prop", () => {
|
||||||
|
const spec = render(<MonospaceText content="code()" style="text-xs" />)
|
||||||
|
|
||||||
|
const root = spec.elements[spec.root]!
|
||||||
|
expect(root.props.style).toBe("text-xs")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("composite", () => {
|
||||||
|
test("FeedCard with nested children", () => {
|
||||||
|
const spec = render(
|
||||||
|
<FeedCard>
|
||||||
|
<SerifText content="Weather" />
|
||||||
|
<SansSerifText content="Sunny, 22C" />
|
||||||
|
<Button label="Details" />
|
||||||
|
</FeedCard>,
|
||||||
|
)
|
||||||
|
|
||||||
|
const root = spec.elements[spec.root]!
|
||||||
|
expect(root.type).toBe("FeedCard")
|
||||||
|
expect(root.children).toHaveLength(3)
|
||||||
|
|
||||||
|
const childKeys = root.children!
|
||||||
|
const child0 = spec.elements[childKeys[0]!]!
|
||||||
|
const child1 = spec.elements[childKeys[1]!]!
|
||||||
|
const child2 = spec.elements[childKeys[2]!]!
|
||||||
|
|
||||||
|
expect(child0.type).toBe("SerifText")
|
||||||
|
expect(child0.props).toEqual({ content: "Weather" })
|
||||||
|
|
||||||
|
expect(child1.type).toBe("SansSerifText")
|
||||||
|
expect(child1.props).toEqual({ content: "Sunny, 22C" })
|
||||||
|
|
||||||
|
expect(child2.type).toBe("Button")
|
||||||
|
expect(child2.props).toEqual({ label: "Details" })
|
||||||
|
})
|
||||||
|
})
|
||||||
12
packages/aelis-components/src/feed-card.ts
Normal file
12
packages/aelis-components/src/feed-card.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import type { JrxNode } from "@nym.sh/jrx"
|
||||||
|
|
||||||
|
import { jsx } from "@nym.sh/jrx/jsx-runtime"
|
||||||
|
|
||||||
|
export type FeedCardProps = {
|
||||||
|
style?: string
|
||||||
|
children?: JrxNode | JrxNode[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FeedCard(props: FeedCardProps): JrxNode {
|
||||||
|
return jsx("FeedCard", props)
|
||||||
|
}
|
||||||
14
packages/aelis-components/src/index.ts
Normal file
14
packages/aelis-components/src/index.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export type { ButtonProps } from "./button.ts"
|
||||||
|
export { Button } from "./button.ts"
|
||||||
|
|
||||||
|
export type { FeedCardProps } from "./feed-card.ts"
|
||||||
|
export { FeedCard } from "./feed-card.ts"
|
||||||
|
|
||||||
|
export type { SansSerifTextProps } from "./sans-serif-text.ts"
|
||||||
|
export { SansSerifText } from "./sans-serif-text.ts"
|
||||||
|
|
||||||
|
export type { SerifTextProps } from "./serif-text.ts"
|
||||||
|
export { SerifText } from "./serif-text.ts"
|
||||||
|
|
||||||
|
export type { MonospaceTextProps } from "./monospace-text.ts"
|
||||||
|
export { MonospaceText } from "./monospace-text.ts"
|
||||||
13
packages/aelis-components/src/monospace-text.ts
Normal file
13
packages/aelis-components/src/monospace-text.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import type { JrxNode } from "@nym.sh/jrx"
|
||||||
|
|
||||||
|
import { jsx } from "@nym.sh/jrx/jsx-runtime"
|
||||||
|
|
||||||
|
export type MonospaceTextProps = {
|
||||||
|
content?: string
|
||||||
|
style?: string
|
||||||
|
children?: JrxNode | JrxNode[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MonospaceText(props: MonospaceTextProps): JrxNode {
|
||||||
|
return jsx("MonospaceText", props)
|
||||||
|
}
|
||||||
13
packages/aelis-components/src/sans-serif-text.ts
Normal file
13
packages/aelis-components/src/sans-serif-text.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import type { JrxNode } from "@nym.sh/jrx"
|
||||||
|
|
||||||
|
import { jsx } from "@nym.sh/jrx/jsx-runtime"
|
||||||
|
|
||||||
|
export type SansSerifTextProps = {
|
||||||
|
content?: string
|
||||||
|
style?: string
|
||||||
|
children?: JrxNode | JrxNode[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SansSerifText(props: SansSerifTextProps): JrxNode {
|
||||||
|
return jsx("SansSerifText", props)
|
||||||
|
}
|
||||||
13
packages/aelis-components/src/serif-text.ts
Normal file
13
packages/aelis-components/src/serif-text.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import type { JrxNode } from "@nym.sh/jrx"
|
||||||
|
|
||||||
|
import { jsx } from "@nym.sh/jrx/jsx-runtime"
|
||||||
|
|
||||||
|
export type SerifTextProps = {
|
||||||
|
content?: string
|
||||||
|
style?: string
|
||||||
|
children?: JrxNode | JrxNode[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SerifText(props: SerifTextProps): JrxNode {
|
||||||
|
return jsx("SerifText", props)
|
||||||
|
}
|
||||||
7
packages/aelis-components/tsconfig.json
Normal file
7
packages/aelis-components/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsxImportSource": "@nym.sh/jrx"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -79,6 +79,12 @@ export interface FeedItem<
|
|||||||
slots?: Record<string, Slot>
|
slots?: Record<string, Slot>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Takes a FeedItem and returns a JRX node tree for rendering. */
|
||||||
|
export type FeedItemRenderer<
|
||||||
|
TType extends string = string,
|
||||||
|
TData extends Record<string, unknown> = Record<string, unknown>,
|
||||||
|
> = (item: FeedItem<TType, TData>) => JrxNode
|
||||||
|
|
||||||
/** A FeedItem with a JRX UI tree attached for client-side rendering. */
|
/** A FeedItem with a JRX UI tree attached for client-side rendering. */
|
||||||
export interface RenderedFeedItem<
|
export interface RenderedFeedItem<
|
||||||
TType extends string = string,
|
TType extends string = string,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export type { ActionDefinition } from "./action"
|
|||||||
export { UnknownActionError } from "./action"
|
export { UnknownActionError } from "./action"
|
||||||
|
|
||||||
// Feed
|
// Feed
|
||||||
export type { FeedItem, FeedItemSignals, RenderedFeedItem, Slot } from "./feed"
|
export type { FeedItem, FeedItemRenderer, FeedItemSignals, RenderedFeedItem, Slot } from "./feed"
|
||||||
export { TimeRelevance } from "./feed"
|
export { TimeRelevance } from "./feed"
|
||||||
|
|
||||||
// Feed Source
|
// Feed Source
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"test:live": "bun run scripts/test-live.ts"
|
"test:live": "bun run scripts/test-live.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@aelis/components": "workspace:*",
|
||||||
"@aelis/core": "workspace:*",
|
"@aelis/core": "workspace:*",
|
||||||
"ical.js": "^2.1.0",
|
"ical.js": "^2.1.0",
|
||||||
"tsdav": "^2.1.7"
|
"tsdav": "^2.1.7"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export { CalDavCalendarKey, type CalendarContext } from "./calendar-context.ts"
|
export { CalDavCalendarKey, type CalendarContext } from "./calendar-context.ts"
|
||||||
export { CalDavSource, type CalDavSourceOptions } from "./caldav-source.ts"
|
export { CalDavSource, type CalDavSourceOptions } from "./caldav-source.ts"
|
||||||
export { parseICalEvents, type ICalTimeRange } from "./ical-parser.ts"
|
export { parseICalEvents, type ICalTimeRange } from "./ical-parser.ts"
|
||||||
|
export { renderCalDavFeedItem } from "./renderer.tsx"
|
||||||
export {
|
export {
|
||||||
AttendeeRole,
|
AttendeeRole,
|
||||||
AttendeeStatus,
|
AttendeeStatus,
|
||||||
|
|||||||
74
packages/aelis-source-caldav/src/renderer.tsx
Normal file
74
packages/aelis-source-caldav/src/renderer.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
/** @jsxImportSource @nym.sh/jrx */
|
||||||
|
|
||||||
|
import type { FeedItemRenderer } from "@aelis/core"
|
||||||
|
|
||||||
|
import { FeedCard, SansSerifText, SerifText } from "@aelis/components"
|
||||||
|
|
||||||
|
import type { CalDavEventData } from "./types.ts"
|
||||||
|
|
||||||
|
import { CalDavEventStatus } from "./types.ts"
|
||||||
|
|
||||||
|
function formatTime(date: Date): string {
|
||||||
|
const hours = date.getHours()
|
||||||
|
const minutes = date.getMinutes()
|
||||||
|
const period = hours >= 12 ? "PM" : "AM"
|
||||||
|
const h = hours % 12 || 12
|
||||||
|
const m = minutes.toString().padStart(2, "0")
|
||||||
|
return `${h}:${m} ${period}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimeRange(data: CalDavEventData): string {
|
||||||
|
if (data.isAllDay) {
|
||||||
|
return "All day"
|
||||||
|
}
|
||||||
|
return `${formatTime(data.startDate)} – ${formatTime(data.endDate)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatStatus(status: CalDavEventData["status"]): string | null {
|
||||||
|
if (status === CalDavEventStatus.Cancelled) return "Cancelled"
|
||||||
|
if (status === CalDavEventStatus.Tentative) return "Tentative"
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const renderCalDavFeedItem: FeedItemRenderer<"caldav-event", CalDavEventData> = (item) => {
|
||||||
|
const { data, slots } = item
|
||||||
|
const statusLabel = formatStatus(data.status)
|
||||||
|
const attendeeCount = data.attendees.length
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FeedCard>
|
||||||
|
{statusLabel ? <SansSerifText content={statusLabel} style="text-xs uppercase" /> : null}
|
||||||
|
|
||||||
|
<SerifText content={data.title} style="text-lg" />
|
||||||
|
|
||||||
|
<SansSerifText content={formatTimeRange(data)} style="text-sm" />
|
||||||
|
|
||||||
|
{data.calendarName ? (
|
||||||
|
<SansSerifText content={data.calendarName} style="text-sm text-secondary" />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{data.location ? (
|
||||||
|
<SansSerifText content={data.location} style="text-sm text-secondary" />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{attendeeCount > 0 ? (
|
||||||
|
<SansSerifText
|
||||||
|
content={`${attendeeCount} attendee${attendeeCount === 1 ? "" : "s"}`}
|
||||||
|
style="text-sm text-secondary"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{slots?.insight?.content ? (
|
||||||
|
<SansSerifText content={slots.insight.content} style="text-sm" />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{slots?.preparation?.content ? (
|
||||||
|
<SansSerifText content={slots.preparation.content} style="text-sm" />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{slots?.crossSource?.content ? (
|
||||||
|
<SansSerifText content={slots.crossSource.content} style="text-sm" />
|
||||||
|
) : null}
|
||||||
|
</FeedCard>
|
||||||
|
)
|
||||||
|
}
|
||||||
7
packages/aelis-source-caldav/tsconfig.json
Normal file
7
packages/aelis-source-caldav/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsxImportSource": "@nym.sh/jrx"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user