diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 31b26246..30d680ed 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -2,6 +2,6 @@ class ApplicationController < ActionController::Base include Shimmer::Localizable - include Shimmer::RemoteNavigation + include Reaction::Controller include Pundit::Authorization end diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index a1949aa4..6fec03d9 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -2,30 +2,6 @@ require "benchmark" -class TSXHandler - class << self - def render(path, assigns) - id = Pathname.new(path).relative_path_from(Rails.root.join("app", "views")).to_s - schema = PropSchema.new(path.sub(".tsx", ".props.rb")) - schema_file_path = path.sub(".tsx", ".schema.ts") - File.write(schema_file_path, schema.to_typescript) - props = schema.serialize(assigns).deep_transform_keys { _1.to_s.camelize(:lower) } - response = nil - time = Benchmark.realtime do - response = HTTParty.post("http://localhost:4000/render", body: {path:, props:, id:}.to_json) - end - puts "took #{(time * 1000).round(2)}ms to render" - response.body - end - end - - def call(template, source) - "TSXHandler.render('#{template.identifier}', assigns)" - end -end - -ActionView::Template.register_template_handler(:tsx, TSXHandler.new) - class PagesController < ApplicationController before_action :authenticate_user! diff --git a/app/javascript/application.ts b/app/javascript/application.ts index 0b14be8c..5d890040 100644 --- a/app/javascript/application.ts +++ b/app/javascript/application.ts @@ -1,13 +1,4 @@ -import '@hotwired/turbo-rails'; -import { start } from '@nerdgeschoss/shimmer'; -import { Application } from '@hotwired/stimulus'; -import { registerControllers } from 'stimulus-vite-helpers'; -import 'chartkick/chart.js'; +import { Reaction } from './sprinkles/reaction'; -const application = Application.start(); -const controllers = import.meta.glob('./controllers/**/*_controller.{ts,tsx}', { - eager: true, -}); -registerControllers(application, controllers); - -start({ application }); +const reaction = new Reaction(); +reaction.start(); diff --git a/app/javascript/components/sidebar/sidebar.tsx b/app/javascript/components/sidebar/sidebar.tsx new file mode 100644 index 00000000..598212c4 --- /dev/null +++ b/app/javascript/components/sidebar/sidebar.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import logo from '../../../frontend/images/logo.svg'; + +export function Sidebar(): JSX.Element { + return ( + + ); +} diff --git a/app/javascript/sprinkles/history.ts b/app/javascript/sprinkles/history.ts new file mode 100644 index 00000000..55bd316e --- /dev/null +++ b/app/javascript/sprinkles/history.ts @@ -0,0 +1,24 @@ +import { Meta } from './meta'; +import { MetaCache } from './meta_cache'; + +export class History { + cache = new MetaCache(); + + constructor(private onChange: (meta: Meta) => void) { + window.addEventListener('popstate', this.restore.bind(this)); + } + + async navigate(url: string): Promise { + const result = await this.cache.fetch(url); + window.history.pushState({}, '', url); + this.onChange(result.meta); + if (result.fresh) return; + this.onChange(await this.cache.refresh(url)); + } + + async restore(): Promise { + const url = window.location.pathname + window.location.search; + const result = await this.cache.fetch(url); + this.onChange(result.meta); + } +} diff --git a/app/javascript/sprinkles/meta.ts b/app/javascript/sprinkles/meta.ts new file mode 100644 index 00000000..b91f4480 --- /dev/null +++ b/app/javascript/sprinkles/meta.ts @@ -0,0 +1,9 @@ +export class Meta { + component: string; + props: any; + + constructor(data: any) { + this.component = data.component; + this.props = data.props; + } +} diff --git a/app/javascript/sprinkles/meta_cache.ts b/app/javascript/sprinkles/meta_cache.ts new file mode 100644 index 00000000..dc0a5a33 --- /dev/null +++ b/app/javascript/sprinkles/meta_cache.ts @@ -0,0 +1,37 @@ +import { Meta } from './meta'; + +interface CacheResult { + meta: Meta; + fresh: boolean; +} + +export class MetaCache { + private cache = new Map(); + + async fetch(url: string): Promise { + console.log('fetch', url, this.cache.has(url)); + if (this.cache.has(url)) { + return { meta: this.cache.get(url)!, fresh: false }; + } + return { meta: await this.refresh(url), fresh: true }; + } + + async refresh(url: string): Promise { + console.log('refreshing', url); + const response = await fetch(url, { + headers: { accept: 'application/json' }, + }); + const body = await response.json(); + const meta = new Meta(body); + this.cache.set(url, meta); + return body; + } + + write(url: string, data: Meta): void { + this.cache.set(url, data); + } + + clear(): void { + this.cache.clear(); + } +} diff --git a/app/javascript/sprinkles/reaction.ts b/app/javascript/sprinkles/reaction.ts new file mode 100644 index 00000000..793d740e --- /dev/null +++ b/app/javascript/sprinkles/reaction.ts @@ -0,0 +1,48 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { History } from './history'; +import { Meta } from './meta'; + +const imports = import.meta.glob('../../views/**/*.tsx', {}); + +export class Reaction { + private root!: ReturnType; + history = new History((meta) => this.renderPage(meta)); + + start(): void { + document.addEventListener('DOMContentLoaded', () => { + this.loadPage(); + }); + document.addEventListener('click', (event) => { + let target = event.target as HTMLAnchorElement; + while (target && target.tagName !== 'A') { + if (target === document.body) return; + target = target.parentElement as HTMLAnchorElement; + } + if (!target) return; + event.preventDefault(); + this.history.navigate(target.getAttribute('href')!); + }); + } + + private async loadPage(): Promise { + const rootElement = document.getElementById('root'); + if (!rootElement) throw new Error('Root element not found'); + const metaJson = ( + document.querySelector('meta[name="reaction-data"]') as HTMLMetaElement + ).content; + const data = metaJson ? JSON.parse(metaJson) : {}; + const meta = new Meta(data); + this.history.cache.write(window.location.pathname, meta); + this.root = createRoot(rootElement); + this.renderPage(meta); + } + + private async renderPage(meta: Meta): Promise { + const importPath = '../../views/' + meta.component + '.tsx'; + const implementation = imports[importPath]; + if (!implementation) return; + const App = ((await implementation()) as any).default; + this.root.render(React.createElement(App, { data: meta.props })); + } +} diff --git a/app/models/prop_field.rb b/app/models/prop_field.rb deleted file mode 100644 index 45c2de69..00000000 --- a/app/models/prop_field.rb +++ /dev/null @@ -1,67 +0,0 @@ -class PropField - attr_reader :name, :type, :null, :fields - - def initialize(name, type = nil, null: true, &block) - @name = name - @type = block ? Object : type - @null = null - @fields = {} - instance_exec(&block) if block - end - - def serialize(value) - return nil if value.nil? && null - - if type.nil? - value - elsif type == String - value.to_s - elsif type == Integer - value.to_i - elsif type == Float - value.to_f - elsif type == Date - value.to_s - elsif type == Time - value.iso8601 - elsif type == Object - fields.map do |name, field| - field_value = value.is_a?(Hash) ? value.with_indifferent_access[name] : value.try(name) - [name, field.serialize(field_value)] - end.to_h - else - raise "Unknown type: #{type}" - end - end - - def to_typescript(skip_root: false) - name = self.name.to_s.camelize(:lower) - if type.nil? - "#{name}: unknown#{null ? " | null" : ""};" - elsif type == String - "#{name}: string#{null ? " | null" : ""};" - elsif type == Integer - "#{name}: number#{null ? " | null" : ""};" - elsif type == Float - "#{name}: number#{null ? " | null" : ""};" - elsif type == Date - "#{name}: string#{null ? " | null" : ""};" - elsif type == Time - "#{name}: string#{null ? " | null" : ""};" - elsif type == Object - if skip_root - fields.map { |name, field| field.to_typescript }.join("\n") - else - "#{name}: {\n#{fields.map { |name, field| field.to_typescript.indent(2) }.join("\n")}\n}#{null ? " | null" : ""};" - end - else - raise "Unknown type: #{type}" - end - end - - private - - def field(name, type = nil, null: true, &) - @fields[name] = PropField.new(name, type, null:, &) - end -end diff --git a/app/views/layouts/application.html.slim b/app/views/layouts/application.html.slim index 9a13164a..cc954c5f 100644 --- a/app/views/layouts/application.html.slim +++ b/app/views/layouts/application.html.slim @@ -2,6 +2,4 @@ doctype html html lang=I18n.locale = render "components/head" body class=Rails.env - = render "components/flash" - = render "components/sidebar" if current_user - main.content = yield + = yield diff --git a/app/views/pages/home.html.slim b/app/views/pages/_home.html.slim similarity index 100% rename from app/views/pages/home.html.slim rename to app/views/pages/_home.html.slim diff --git a/app/views/pages/home.props.rb b/app/views/pages/home.props.rb index f65ee4ae..5be5d53e 100644 --- a/app/views/pages/home.props.rb +++ b/app/views/pages/home.props.rb @@ -1,8 +1,5 @@ -field :current_user, null: false do - field :id, String, null: false - field :first_name, String, null: false -end -field :sprint do - field :id, String, null: false - field :title, String, null: false +field :current_user, global: :current_user do + field :id + field :email + field :first_name end diff --git a/app/views/pages/home.schema.ts b/app/views/pages/home.schema.ts deleted file mode 100644 index c0344c73..00000000 --- a/app/views/pages/home.schema.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface Props { - currentUser: { - id: string; - firstName: string; - }; - sprint: { - id: string; - title: string; - } | null; -} diff --git a/app/views/pages/home.tsx b/app/views/pages/home.tsx index 5c39d934..23eb7069 100644 --- a/app/views/pages/home.tsx +++ b/app/views/pages/home.tsx @@ -1,14 +1,20 @@ import React, { useState } from 'react'; -import { Props } from './home.schema.js'; +import { PageProps } from '../../../data.d'; +import { Sidebar } from '../../javascript/components/sidebar/sidebar'; -export default function Home({ currentUser }: Props): JSX.Element { +export default function Home({ + data: { currentUser }, +}: PageProps<'pages/home'>): JSX.Element { const [counter, setCounter] = useState(0); return ( -
-

Home

-

Welcome to the home page, {currentUser.firstName}

-

Counter: {counter}

- -
+ <> + +
+

Home

+

Welcome to the home page, {currentUser.firstName}

+

Counter: {counter}

+ +
+ ); } diff --git a/app/views/users/index.html.slim b/app/views/users/index.html.slim deleted file mode 100644 index ff289261..00000000 --- a/app/views/users/index.html.slim +++ /dev/null @@ -1,7 +0,0 @@ -.container: .stack - h1.headline = t ".users" - .line.line--space-between - .stack.stack--row.stack--small.stack--wrap - - (["employee", "sprinter", "hr", "archive"]).each do |filter| - a.pill class=("active" if filter == @filter) href=(url_for(filter:)) = t("user.filter.#{filter}") - = render @users diff --git a/app/views/users/index.props.rb b/app/views/users/index.props.rb new file mode 100644 index 00000000..0be1915c --- /dev/null +++ b/app/views/users/index.props.rb @@ -0,0 +1,12 @@ +field :filter, value: -> { @filter } +field :users, array: true, value: -> { @users } do + field :id + field :avatar_url, value: -> { avatar_image(size: 80) } + field :full_name + field :nick_name, null: true + field :remaining_holidays, Integer + field :current_salary, null: true do + field :brut, Float + field :valid_from, Date + end +end diff --git a/app/views/users/index.tsx b/app/views/users/index.tsx new file mode 100644 index 00000000..0b80de6e --- /dev/null +++ b/app/views/users/index.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { Sidebar } from '../../javascript/components/sidebar/sidebar'; +import { PageProps } from '../../../data.d'; + +export default function ({ + data: { filter, users }, +}: PageProps<'users/index'>): JSX.Element { + return ( + <> + +
+
+
+

Users

+
+
+ {['employee', 'sprinter', 'hr', 'archive'].map((e) => ( + + {e} + + ))} +
+
+ {users.map((user) => ( +
+
+
+ +
+
+ + {user.fullName} {user.nickName && `(${user.nickName})`} + +
+ {user.remainingHolidays} holidays left + {user.currentSalary && ( +
+ ${user.currentSalary.brut} since $ + {user.currentSalary.validFrom} +
+ )} +
+
+
+
+ ))} +
+
+
+ + ); +} + +// .container: .stack +// h1.headline = t ".users" +// .line.line--space-between +// .stack.stack--row.stack--small.stack--wrap +// - (["employee", "sprinter", "hr", "archive"]).each do |filter| +// a.pill class=("active" if filter == @filter) href=(url_for(filter:)) = t("user.filter.#{filter}") +// = render @users diff --git a/data.d.ts b/data.d.ts new file mode 100644 index 00000000..2fc00b6e --- /dev/null +++ b/data.d.ts @@ -0,0 +1,25 @@ +export interface DataSchema { + 'pages/home': { + currentUser: { + id: string; + email: string; + firstName: string; + }; + }; + 'users/index': { + filter: string; + users: Array<{ + id: string; + avatarUrl: string; + fullName: string; + nickName: string | null; + remainingHolidays: number; + currentSalary: { + brut: number; + validFrom: string; + } | null; + }>; + }; +} + +export type PageProps = { data: DataSchema[T] }; diff --git a/lib/reaction.rb b/lib/reaction.rb new file mode 100644 index 00000000..1bd41adf --- /dev/null +++ b/lib/reaction.rb @@ -0,0 +1,2 @@ +module Reaction +end diff --git a/lib/reaction/controller.rb b/lib/reaction/controller.rb new file mode 100644 index 00000000..4f157346 --- /dev/null +++ b/lib/reaction/controller.rb @@ -0,0 +1,20 @@ +module Reaction + module Controller + extend ActiveSupport::Concern + + included do + ActionView::Template.register_template_handler(:tsx, Reaction::TsxHandler.new) + + def default_render + respond_to do |format| + format.html do + super + end + format.json do + render json: Response.new(component: "#{controller_path}/#{action_name}", context: self).to_s + end + end + end + end + end +end diff --git a/lib/reaction/props/field.rb b/lib/reaction/props/field.rb new file mode 100644 index 00000000..5e05c224 --- /dev/null +++ b/lib/reaction/props/field.rb @@ -0,0 +1,82 @@ +module Reaction + module Props + class Field + attr_reader :name, :type, :null, :fields, :value_override, :global, :array + + def initialize(name, type = String, null: false, value: nil, global: nil, array: false, &block) + @name = name + @type = block ? Object : type + @null = null + @value_override = value + @fields = {} + @global = global + @array = array + instance_exec(&block) if block + end + + def serialize(value, array_content: false) + return nil if value.nil? && null + + if array && !array_content + return value.map { |v| serialize(v, array_content: true) } + end + + if type.nil? + value + elsif type == String + value.as_json.to_s + elsif type == Integer + value.to_i + elsif type == Float + value.to_f + elsif type == Date + value.to_s + elsif type == Time + value.iso8601 + elsif type == Object + fields.map do |name, field| + field_value = if field.value_override + value.instance_exec(&field.value_override) + else + value.is_a?(Hash) ? value.with_indifferent_access[name] : value.try(name) + end + [name.to_s, field.serialize(field_value)] + end.to_h + else + raise "Unknown type: #{type}" + end + end + + def to_typescript(skip_root: false, array_content: false) + name = self.name.to_s.camelize(:lower) + if array && !array_content + "#{name}: Array<{#{to_typescript(skip_root: true, array_content: true)}}>" + elsif type == String + "#{name}: string#{null ? " | null" : ""};" + elsif type == Integer + "#{name}: number#{null ? " | null" : ""};" + elsif type == Float + "#{name}: number#{null ? " | null" : ""};" + elsif type == Date + "#{name}: string#{null ? " | null" : ""};" + elsif type == Time + "#{name}: string#{null ? " | null" : ""};" + elsif type == Object + if skip_root + fields.map { |name, field| field.to_typescript }.join("\n") + else + "#{name}: {\n#{fields.map { |name, field| field.to_typescript.indent(2) }.join("\n")}\n}#{null ? " | null" : ""};" + end + else + raise "Unknown type: #{type}" + end + end + + private + + def field(name, type = String, null: false, value: nil, global: nil, array: false, &) + @fields[name] = self.class.new(name, type, null:, value:, global:, array:, &) + end + end + end +end diff --git a/lib/reaction/props/schema.rb b/lib/reaction/props/schema.rb new file mode 100644 index 00000000..d5f4f549 --- /dev/null +++ b/lib/reaction/props/schema.rb @@ -0,0 +1,24 @@ +module Reaction + module Props + class Schema + attr_reader :root + + def initialize(string) + @root = Field.new(:root, Object, null: false) + @root.instance_exec { binding.eval(string) } + end + + def serialize(object) + root.serialize(object).deep_transform_keys { _1.to_s.camelize(:lower) } + end + + def to_typescript + <<~TS + export interface Props { + #{root.to_typescript(skip_root: true)} + } + TS + end + end + end +end diff --git a/lib/reaction/response.rb b/lib/reaction/response.rb new file mode 100644 index 00000000..14f36d4f --- /dev/null +++ b/lib/reaction/response.rb @@ -0,0 +1,21 @@ +module Reaction + class Response + attr_reader :schema, :component + + def initialize(component:, context:) + @context = context + @component = component + @schema = Reaction::Props::Schema.new(File.read(Rails.root.join("app", "views", "#{component}.props.rb"))) + end + + def to_s + props = schema.serialize(@context) + globals = schema.root.fields.values.select { _1.global.present? }.map { [_1.name, _1.global] }.to_h + { + component:, + props:, + globals: + }.to_json + end + end +end diff --git a/lib/reaction/tsx_handler.rb b/lib/reaction/tsx_handler.rb new file mode 100644 index 00000000..71f3e7b6 --- /dev/null +++ b/lib/reaction/tsx_handler.rb @@ -0,0 +1,18 @@ +module Reaction + class TsxHandler + class << self + def render(path, assigns, context) + id = Pathname.new(path).relative_path_from(Rails.root.join("app", "views")).to_s.sub(".tsx", "") + response = Response.new(component: id, context:) + <<~HTML + +
+ HTML + end + end + + def call(template, source) + "Reaction::TsxHandler.render('#{template.identifier}', assigns, self)" + end + end +end diff --git a/lib/tasks/schema.rake b/lib/tasks/schema.rake new file mode 100644 index 00000000..4885a9ad --- /dev/null +++ b/lib/tasks/schema.rake @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +namespace :schema do + desc "writes the schema" + task generate: :environment do + schemas = {} + Dir.glob(Rails.root.join("app", "views", "**", "*.props.rb")).each do |path| + name = Pathname.new(path).relative_path_from(Rails.root.join("app", "views")).to_s.sub(".props.rb", "") + schema = Reaction::Props::Schema.new(File.read(path)) + schemas[name] = schema.root.to_typescript(skip_root: true) + end + content = <<~TS + export interface DataSchema { + #{schemas.map { |name, schema| "\"#{name}\": {#{schema}}" }.join("\n")} + } + + export type PageProps = { data: DataSchema[T] }; + TS + schema_path = Rails.root.join("data.d.ts") + File.write(schema_path, content) + sh "yarn prettier --write #{schema_path}" + end +end diff --git a/package.json b/package.json index 51979a92..4c5386b2 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,13 @@ "@hotwired/stimulus": "^3.2.2", "@hotwired/turbo-rails": "^8.0.12", "@nerdgeschoss/shimmer": "^0.0.10", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", "chart.js": "^3.7.0", "chartkick": "^4.1.1", "flatpickr": "^4.6.9", + "react": "^18.3.1", + "react-dom": "^18.3.1", "sass": "^1.77.6", "typescript": "^4.5.4", "vite": "^5.0.0", diff --git a/server.tsx b/server.tsx deleted file mode 100644 index 44ed8f4a..00000000 --- a/server.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import { renderToString } from 'react-dom/server'; -// import App from '#{path.delete_suffix(".tsx")}'; -// const props = #{assigns.except("ui").as_json.deep_transform_keys { |key| key.camelize(:lower) }.to_json}; -// await Bun.write("#{result.path}", renderToString()); - -const cache = {}; - -const server = Bun.serve({ - port: 4000, - async fetch(request: Request) { - const body = await request.json(); - const path = body.path; - const id = body.id; - let module = cache[path]; - if (!module) { - module = await import(path); - cache[path] = module; - } - const App = module.default; - console.log('rendering', id, body.props); - const content = renderToString(); - const page = ` -
- ${content} -
- `; - return new Response(page); - }, -}); diff --git a/spec/fixtures/files/example_schema.rb b/spec/fixtures/files/example_schema.rb new file mode 100644 index 00000000..56097b51 --- /dev/null +++ b/spec/fixtures/files/example_schema.rb @@ -0,0 +1,6 @@ +field :current_user do + field :first_name + field :email, null: false + field :age, Integer +end +field :sprint, null: false, value: -> { sprint2 } diff --git a/spec/models/prop_schema_spec.rb b/spec/models/prop_schema_spec.rb new file mode 100644 index 00000000..0be66996 --- /dev/null +++ b/spec/models/prop_schema_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe PropSchema do + describe "#initialize" do + it "parses a tree of properties using a DSL" do + schema = PropSchema.new(Rails.root.join("spec/fixtures/files/example_schema.rb")) + root = schema.root + current_user = root.fields[:current_user] + expect(root.name).to eq :root + expect(root.type).to eq Object + expect(root.null).to eq false + expect(root.fields.keys).to match_array [:current_user, :sprint] + expect(current_user.type).to eq Object + expect(current_user.fields.keys).to match_array [:first_name, :email, :age] + expect(current_user.fields[:first_name].type).to eq String + expect(current_user.fields[:age].type).to eq Integer + end + + describe "#serialize" do + it "serializes a tree of properties" do + schema = PropSchema.new(Rails.root.join("spec/fixtures/files/example_schema.rb")) + serialized = schema.serialize(OpenStruct.new({ + current_user: { + first_name: "John", + email: "name@example.com", + age: "30" + }, sprint2: "some value" + })) + expect(serialized["current_user"]).to eq({ + "first_name" => "John", + "email" => "name@example.com", + "age" => 30 + }) + expect(serialized["sprint"]).to eq "some value" + end + end + end +end diff --git a/tsconfig.json b/tsconfig.json index ca58251d..85309b7b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,7 +25,8 @@ "strict": true }, "files": [ - "typings.d.ts" + "typings.d.ts", + "data.d.ts" ], "include": [ "app/javascript/**/*" diff --git a/yarn.lock b/yarn.lock index c5cb3a0b..c3df57ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -314,6 +314,26 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== +"@types/prop-types@*": + version "15.7.13" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.13.tgz#2af91918ee12d9d32914feb13f5326658461b451" + integrity sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA== + +"@types/react-dom@^18.3.1": + version "18.3.1" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.1.tgz#1e4654c08a9cdcfb6594c780ac59b55aad42fe07" + integrity sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ== + dependencies: + "@types/react" "*" + +"@types/react@*", "@types/react@^18.3.12": + version "18.3.12" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.12.tgz#99419f182ccd69151813b7ee24b792fe08774f60" + integrity sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + "@typescript-eslint/eslint-plugin@^5.8.1": version "5.8.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.8.1.tgz#97dfaa39f38e99f86801fdf34f9f1bed66704258" @@ -549,6 +569,11 @@ cross-spawn@^7.0.2: shebang-command "^2.0.0" which "^2.0.1" +csstype@^3.0.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + date-fns@>=2.0.0: version "2.28.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2" @@ -990,6 +1015,11 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" @@ -1020,6 +1050,13 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +loose-envify@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -1176,6 +1213,21 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +react-dom@^18.3.1: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" + integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.2" + +react@^18.3.1: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" + integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== + dependencies: + loose-envify "^1.1.0" + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -1253,6 +1305,13 @@ sass@^1.77.6: immutable "^4.0.0" source-map-js ">=0.6.2 <2.0.0" +scheduler@^0.23.2: + version "0.23.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" + integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== + dependencies: + loose-envify "^1.1.0" + semver@^7.2.1, semver@^7.3.5: version "7.3.5" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"