├── .gitignore ├── .vscode └── extensions.json ├── README.md ├── android ├── .gitignore ├── app │ ├── .gitignore │ ├── build.gradle │ ├── capacitor.build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── getcapacitor │ │ │ └── myapp │ │ │ └── ExampleInstrumentedTest.java │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── dev │ │ │ │ └── adoe │ │ │ │ └── perfice │ │ │ │ └── MainActivity.java │ │ └── res │ │ │ ├── drawable-land-hdpi │ │ │ └── splash.png │ │ │ ├── drawable-land-ldpi │ │ │ └── splash.png │ │ │ ├── drawable-land-mdpi │ │ │ └── splash.png │ │ │ ├── drawable-land-night-hdpi │ │ │ └── splash.png │ │ │ ├── drawable-land-night-ldpi │ │ │ └── splash.png │ │ │ ├── drawable-land-night-mdpi │ │ │ └── splash.png │ │ │ ├── drawable-land-night-xhdpi │ │ │ └── splash.png │ │ │ ├── drawable-land-night-xxhdpi │ │ │ └── splash.png │ │ │ ├── drawable-land-night-xxxhdpi │ │ │ └── splash.png │ │ │ ├── drawable-land-xhdpi │ │ │ └── splash.png │ │ │ ├── drawable-land-xxhdpi │ │ │ └── splash.png │ │ │ ├── drawable-land-xxxhdpi │ │ │ └── splash.png │ │ │ ├── drawable-night │ │ │ └── splash.png │ │ │ ├── drawable-port-hdpi │ │ │ └── splash.png │ │ │ ├── drawable-port-ldpi │ │ │ └── splash.png │ │ │ ├── drawable-port-mdpi │ │ │ └── splash.png │ │ │ ├── drawable-port-night-hdpi │ │ │ └── splash.png │ │ │ ├── drawable-port-night-ldpi │ │ │ └── splash.png │ │ │ ├── drawable-port-night-mdpi │ │ │ └── splash.png │ │ │ ├── drawable-port-night-xhdpi │ │ │ └── splash.png │ │ │ ├── drawable-port-night-xxhdpi │ │ │ └── splash.png │ │ │ ├── drawable-port-night-xxxhdpi │ │ │ └── splash.png │ │ │ ├── drawable-port-xhdpi │ │ │ └── splash.png │ │ │ ├── drawable-port-xxhdpi │ │ │ └── splash.png │ │ │ ├── drawable-port-xxxhdpi │ │ │ └── splash.png │ │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── drawable │ │ │ ├── ic_launcher_background.xml │ │ │ ├── small_icon.png │ │ │ └── splash.png │ │ │ ├── layout │ │ │ └── activity_main.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-ldpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ │ ├── values │ │ │ ├── ic_launcher_background.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ │ └── xml │ │ │ └── file_paths.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── getcapacitor │ │ └── myapp │ │ └── ExampleUnitTest.java ├── build.gradle ├── capacitor.settings.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── variables.gradle ├── assets ├── icon-background.png ├── icon-foreground.png ├── icon-only.png ├── splash-dark.png └── splash.png ├── capacitor.config.ts ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── analytics-onboarding-mobile.png ├── analytics-onboarding.png ├── dashboard-onboarding-mobile.png ├── dashboard-onboarding.png ├── favicon-128x128.png ├── favicon-512x512.png ├── favicon-64x64.png ├── goals-onboarding-mobile.png ├── goals-onboarding.png ├── sw.js ├── tags-onboarding-mobile.png ├── tags-onboarding.png ├── trackables-onboarding-mobile.png └── trackables-onboarding.png ├── src ├── App.svelte ├── app.css ├── app.ts ├── assets │ ├── OpenMoji-black-glyf.woff2 │ ├── dashboard_suggestions.json │ ├── emojis.json │ ├── goal_suggestions.json │ ├── reflection_suggestions.json │ ├── tag_suggestions.json │ └── trackable_suggestions.json ├── components │ ├── QuickLogField.svelte │ ├── analytics │ │ ├── AnalyticsCorrelationView.svelte │ │ ├── Heatmap.svelte │ │ ├── NewCorrelations.svelte │ │ ├── QuestionLabel.svelte │ │ ├── details │ │ │ ├── CorrelationAnalytics.svelte │ │ │ ├── CorrelationBar.svelte │ │ │ ├── CorrelationCard.svelte │ │ │ ├── CorrelationMessage.svelte │ │ │ ├── CorrelationMessagePart.svelte │ │ │ ├── tag │ │ │ │ ├── AnalyticsTagDetailsView.svelte │ │ │ │ └── TagWeekDayAnalytics.svelte │ │ │ └── trackable │ │ │ │ ├── AnalyticsTrackableDetailsView.svelte │ │ │ │ ├── BasicCategoricalAnalyticsRow.svelte │ │ │ │ ├── BasicQuantitativeAnalyticsRow.svelte │ │ │ │ └── TrackableWeekDayAnalytics.svelte │ │ ├── modal │ │ │ └── EditAnalyticsSettingsModal.svelte │ │ ├── tag │ │ │ └── AnalyticsTagView.svelte │ │ └── trackable │ │ │ ├── AnalyticsTrackableCard.svelte │ │ │ ├── AnalyticsTrackableLineChart.svelte │ │ │ └── AnalyticsTrackableView.svelte │ ├── base │ │ ├── button │ │ │ ├── Button.svelte │ │ │ ├── CardButton.svelte │ │ │ ├── HorizontalPlusButton.svelte │ │ │ ├── IconButton.svelte │ │ │ ├── LineButton.svelte │ │ │ ├── PopupIconButton.svelte │ │ │ └── SelectCardButton.svelte │ │ ├── calendarScroll │ │ │ ├── CalendarScroll.svelte │ │ │ └── CalendarScrollItem.svelte │ │ ├── card │ │ │ ├── GenericActionsCard.svelte │ │ │ ├── GenericEditDeleteCard.svelte │ │ │ └── TitledCard.svelte │ │ ├── category │ │ │ ├── CategoryContainerHeader.svelte │ │ │ └── GenericCategoryContainer.svelte │ │ ├── color │ │ │ └── ColorPickerButton.svelte │ │ ├── contextMenu │ │ │ ├── ContextMenu.svelte │ │ │ ├── ContextMenuButtons.svelte │ │ │ └── PopupContextMenuButton.svelte │ │ ├── datePicker │ │ │ └── DatePicker.svelte │ │ ├── dnd │ │ │ └── DragAndDropContainer.svelte │ │ ├── dropdown │ │ │ ├── BindableDropdownButton.svelte │ │ │ ├── DropdownButton.svelte │ │ │ └── MultiSelectDropdownButton.svelte │ │ ├── dynamic │ │ │ ├── DynamicInput.svelte │ │ │ └── DynamicLabel.svelte │ │ ├── fileButton │ │ │ └── FileButton.svelte │ │ ├── gesture │ │ │ └── SwipeDetector.svelte │ │ ├── icon │ │ │ ├── Icon.svelte │ │ │ └── icons.ts │ │ ├── iconLabel │ │ │ ├── IconLabel.svelte │ │ │ └── IconLabelBetween.svelte │ │ ├── iconPicker │ │ │ ├── IconPicker.svelte │ │ │ └── IconPickerButton.svelte │ │ ├── inline │ │ │ ├── InlineCreateInput.svelte │ │ │ └── InlineCreateLineButton.svelte │ │ ├── invertedSegmented │ │ │ ├── InvertedSegment.svelte │ │ │ └── InvertedSegmentedControl.svelte │ │ ├── modal │ │ │ ├── MobileModalHeader.svelte │ │ │ ├── Modal.svelte │ │ │ ├── ModalFooter.svelte │ │ │ ├── ModalFooterContainer.svelte │ │ │ └── generic │ │ │ │ ├── GenericDeleteModal.svelte │ │ │ │ └── GenericEntityModal.svelte │ │ ├── progress │ │ │ └── CircularProgressBar.svelte │ │ ├── segmented │ │ │ ├── Segment.svelte │ │ │ └── SegmentedControl.svelte │ │ ├── sidebar │ │ │ └── Sidebar.svelte │ │ ├── textOrDynamic │ │ │ └── EditTextOrDynamic.svelte │ │ ├── timePicker │ │ │ └── TimePicker.svelte │ │ ├── timeScope │ │ │ ├── RangedDatePicker.svelte │ │ │ ├── RangedTimeScopePicker.svelte │ │ │ ├── SimpleTimeScopePicker.svelte │ │ │ └── TimeScopePicker.svelte │ │ ├── title │ │ │ ├── Title.svelte │ │ │ └── TitleAndCalendar.svelte │ │ └── weekDays │ │ │ └── WeekDays.svelte │ ├── chart │ │ ├── CanvasChartRenderer.svelte │ │ ├── DualLineChart.svelte │ │ ├── PieChart.svelte │ │ └── SingleChart.svelte │ ├── dashboard │ │ ├── DashboardSidebar.svelte │ │ ├── DashboardWidgetRenderer.svelte │ │ ├── GridstackGrid.svelte │ │ ├── sidebar │ │ │ ├── add │ │ │ │ ├── AddWidgetSidebar.svelte │ │ │ │ └── DashboardDragInCard.svelte │ │ │ └── edit │ │ │ │ ├── EditWidgetSidebar.svelte │ │ │ │ ├── SelectFormAndQuestion.svelte │ │ │ │ └── types │ │ │ │ ├── chart │ │ │ │ └── EditChartWidgetSidebar.svelte │ │ │ │ ├── checklist │ │ │ │ └── EditChecklistWidgetSidebar.svelte │ │ │ │ ├── entryRow │ │ │ │ └── EditEntryRowWidgetSidebar.svelte │ │ │ │ ├── goal │ │ │ │ └── EditGoalWidgetSidebar.svelte │ │ │ │ ├── insights │ │ │ │ └── EditInsightsWidgetSidebar.svelte │ │ │ │ ├── metric │ │ │ │ └── EditMetricWidgetSidebar.svelte │ │ │ │ ├── newCorrelations │ │ │ │ └── EditNewCorrelationsWidgetSidebar.svelte │ │ │ │ ├── table │ │ │ │ └── EditTableWidgetSidebar.svelte │ │ │ │ ├── tags │ │ │ │ └── EditTagsWidgetSidebar.svelte │ │ │ │ ├── trackable │ │ │ │ └── EditTrackableWidgetSidebar.svelte │ │ │ │ └── welcome │ │ │ │ └── EditWelcomeWidgetSidebar.svelte │ │ └── types │ │ │ ├── chart │ │ │ └── DashboardChartWidget.svelte │ │ │ ├── checkList │ │ │ ├── ChecklistEntry.svelte │ │ │ └── DashboardChecklistWidget.svelte │ │ │ ├── entryRow │ │ │ ├── DashboardEntryRowWidget.svelte │ │ │ └── EntryRowItem.svelte │ │ │ ├── goal │ │ │ └── DashboardGoalWidget.svelte │ │ │ ├── insights │ │ │ └── DashboardInsightsWidget.svelte │ │ │ ├── metric │ │ │ └── DashboardMetricWidget.svelte │ │ │ ├── newCorrelations │ │ │ └── DashboardNewCorrelationsWidget.svelte │ │ │ ├── table │ │ │ ├── DashboardTableWidget.svelte │ │ │ ├── TableWidgetEntry.svelte │ │ │ └── TableWidgetGroupHeader.svelte │ │ │ ├── tags │ │ │ └── DashboardTagsWidget.svelte │ │ │ ├── trackable │ │ │ └── DashboardTrackableWidget.svelte │ │ │ └── welcome │ │ │ └── DashboardWelcomeWidget.svelte │ ├── form │ │ ├── FormEmbed.svelte │ │ ├── editor │ │ │ ├── data │ │ │ │ ├── EditDataQuestionSettings.svelte │ │ │ │ ├── hierarchy │ │ │ │ │ ├── EditHierarchyOption.svelte │ │ │ │ │ ├── EditHierarchyOptionModal.svelte │ │ │ │ │ └── EditHierarchyQuestionSettings.svelte │ │ │ │ ├── number │ │ │ │ │ └── EditNumberQuestionSettings.svelte │ │ │ │ └── text │ │ │ │ │ └── EditTextQuestionSettings.svelte │ │ │ ├── display │ │ │ │ ├── EditDisplayQuestionSettings.svelte │ │ │ │ ├── hierarchy │ │ │ │ │ └── EditHierarchyQuestionDisplaySettings.svelte │ │ │ │ ├── range │ │ │ │ │ └── EditRangeQuestionSettings.svelte │ │ │ │ ├── segmented │ │ │ │ │ ├── EditSegmentedOptionCard.svelte │ │ │ │ │ ├── EditSegmentedOptionModal.svelte │ │ │ │ │ └── EditSegmentedQuestionSettings.svelte │ │ │ │ └── select │ │ │ │ │ ├── EditSelectGrid.svelte │ │ │ │ │ ├── EditSelectOptionCard.svelte │ │ │ │ │ ├── EditSelectOptionModal.svelte │ │ │ │ │ └── EditSelectQuestionSettings.svelte │ │ │ ├── field │ │ │ │ └── FormFieldEdit.svelte │ │ │ └── sidebar │ │ │ │ ├── FormEditorSidebar.svelte │ │ │ │ └── SidebarDropdownHeader.svelte │ │ ├── fields │ │ │ ├── FormFieldRenderer.svelte │ │ │ ├── ValidatedFormField.svelte │ │ │ ├── hierarchy │ │ │ │ ├── HierarchyButton.svelte │ │ │ │ └── HierarchyFormField.svelte │ │ │ ├── input │ │ │ │ ├── BooleanInputFormField.svelte │ │ │ │ ├── DateInputFormField.svelte │ │ │ │ ├── DateTimeInputFormField.svelte │ │ │ │ ├── InputFormField.svelte │ │ │ │ ├── TimeElapsedInputFormField.svelte │ │ │ │ ├── TimeOfDayInputFormField.svelte │ │ │ │ └── VanillaInputFormField.svelte │ │ │ ├── range │ │ │ │ └── RangeFormField.svelte │ │ │ ├── richInput │ │ │ │ └── RichInputFormField.svelte │ │ │ ├── segmented │ │ │ │ └── SegmentedFormField.svelte │ │ │ ├── select │ │ │ │ ├── SelectFormField.svelte │ │ │ │ └── SelectOptionButton.svelte │ │ │ └── textArea │ │ │ │ └── TextAreaFormField.svelte │ │ ├── modals │ │ │ ├── FormModal.svelte │ │ │ └── FormTemplateButton.svelte │ │ └── valueInput │ │ │ ├── FormQuestionValueInput.svelte │ │ │ └── PrimitiveVanillaInputField.svelte │ ├── goal │ │ ├── GoalCard.svelte │ │ ├── GoalCardBase.svelte │ │ ├── GoalMetIndicator.svelte │ │ ├── GoalNewCard.svelte │ │ ├── GoalValueRenderer.svelte │ │ ├── editor │ │ │ ├── GoalConditionCard.svelte │ │ │ ├── condition │ │ │ │ ├── comparison │ │ │ │ │ ├── AddSourceButton.svelte │ │ │ │ │ └── ComparisonConditionRenderer.svelte │ │ │ │ └── goalMet │ │ │ │ │ └── GoalMetConditionRenderer.svelte │ │ │ └── sidebar │ │ │ │ ├── AddConditionSidebar.svelte │ │ │ │ ├── AddSourceSidebar.svelte │ │ │ │ └── GoalEditorSidebar.svelte │ │ ├── multi │ │ │ ├── ComparisonConditionEntry.svelte │ │ │ ├── ConditionEntry.svelte │ │ │ ├── GoalMetConditionEntry.svelte │ │ │ └── MultiConditionRenderer.svelte │ │ └── single │ │ │ ├── ComparisonSingleCondition.svelte │ │ │ ├── GoalMetSingleCondition.svelte │ │ │ └── SingleConditionRenderer.svelte │ ├── import │ │ └── EntryImportResultModal.svelte │ ├── journal │ │ ├── day │ │ │ ├── JournalCardBase.svelte │ │ │ ├── JournalCardHeader.svelte │ │ │ ├── JournalDayCard.svelte │ │ │ ├── JournalDayDate.svelte │ │ │ ├── JournalEntryTimestamp.svelte │ │ │ ├── JournalMultiGroup.svelte │ │ │ ├── JournalSingleGroup.svelte │ │ │ ├── JournalSummaryContainer.svelte │ │ │ └── JournalTagEntries.svelte │ │ └── search │ │ │ ├── JournalSearchEntityCard.svelte │ │ │ ├── common │ │ │ ├── ByCategoryFilterCard.svelte │ │ │ └── OneOfFilterCard.svelte │ │ │ └── types │ │ │ ├── GenericFilterContainer.svelte │ │ │ ├── date │ │ │ └── DateSearchOptions.svelte │ │ │ ├── freeText │ │ │ └── FreeTextSearchOptions.svelte │ │ │ ├── tag │ │ │ ├── TagFilterRenderer.svelte │ │ │ ├── TagSearchActions.svelte │ │ │ ├── TagSearchOptions.svelte │ │ │ └── filters │ │ │ │ ├── TagByCategoryFilterCard.svelte │ │ │ │ └── TagOneOfFilterCard.svelte │ │ │ └── trackable │ │ │ ├── TrackableFilterRenderer.svelte │ │ │ ├── TrackableSearchActions.svelte │ │ │ ├── TrackableSearchOptions.svelte │ │ │ └── filters │ │ │ ├── TrackableByAnswersFilterCard.svelte │ │ │ ├── TrackableByCategoryFilterCard.svelte │ │ │ └── TrackableOneOfFilterCard.svelte │ ├── mobile │ │ └── MobileTopBar.svelte │ ├── onboarding │ │ ├── OnboardingImage.svelte │ │ ├── OnboardingPageNavigation.svelte │ │ ├── OnboardingSelect.svelte │ │ └── OnboardingSelectButton.svelte │ ├── reflection │ │ ├── GlobalReflectionModal.svelte │ │ ├── editor │ │ │ ├── ReflectionPageGroup.svelte │ │ │ ├── notifications │ │ │ │ ├── EditNotificationModal.svelte │ │ │ │ ├── EditReflectionNotifications.svelte │ │ │ │ └── NotificationCard.svelte │ │ │ └── sidebar │ │ │ │ ├── ReflectionEditPageSidebar.svelte │ │ │ │ ├── ReflectionEditWidgetSidebar.svelte │ │ │ │ ├── ReflectionEditorSidebar.svelte │ │ │ │ └── widget │ │ │ │ ├── ReflectionEditChecklistWidget.svelte │ │ │ │ ├── ReflectionEditFormWidget.svelte │ │ │ │ ├── ReflectionEditTableWidget.svelte │ │ │ │ └── ReflectionEditTagsWidget.svelte │ │ └── modal │ │ │ ├── ReflectionModal.svelte │ │ │ ├── ReflectionPageButton.svelte │ │ │ ├── ReflectionPageRenderer.svelte │ │ │ └── widgets │ │ │ ├── ReflectionChecklistWidget.svelte │ │ │ ├── ReflectionFormWidget.svelte │ │ │ ├── ReflectionTableWidget.svelte │ │ │ ├── ReflectionTagsWidget.svelte │ │ │ └── ReflectionWidgetRenderer.svelte │ ├── settings │ │ ├── SettingsDataExport.svelte │ │ ├── SettingsDataImport.svelte │ │ └── SettingsDeleteData.svelte │ ├── sharedWidgets │ │ ├── checklist │ │ │ ├── ChecklistWidget.svelte │ │ │ ├── EditChecklistConditionCard.svelte │ │ │ ├── EditChecklistConditionModal.svelte │ │ │ ├── EditChecklistWidgetSettings.svelte │ │ │ ├── EditFormChecklistCondition.svelte │ │ │ └── EditTagChecklistCondition.svelte │ │ └── table │ │ │ ├── EditTableWidgetSettings.svelte │ │ │ └── TableWidget.svelte │ ├── sidebar │ │ ├── NavigationSidebar.svelte │ │ ├── SidebarButton.svelte │ │ └── drawer │ │ │ ├── DrawerButton.svelte │ │ │ ├── DrawerOpenButton.svelte │ │ │ └── MobileDrawer.svelte │ ├── tag │ │ ├── FilteredTagCategories.svelte │ │ ├── TagButtonBase.svelte │ │ ├── TagCard.svelte │ │ ├── TagCategoryContainer.svelte │ │ ├── TagValueCard.svelte │ │ └── modal │ │ │ └── EditTagModal.svelte │ ├── trackable │ │ ├── TrackableCategoryContainer.svelte │ │ ├── TrackableList.svelte │ │ ├── card │ │ │ ├── TrackableCard.svelte │ │ │ ├── chart │ │ │ │ └── ChartTrackableRenderer.svelte │ │ │ ├── tally │ │ │ │ └── TallyTrackableRenderer.svelte │ │ │ └── value │ │ │ │ ├── ValueTrackableRenderer.svelte │ │ │ │ ├── latest │ │ │ │ └── LatestTrackableRenderer.svelte │ │ │ │ └── table │ │ │ │ ├── TableTrackableRenderer.svelte │ │ │ │ └── TrackableTableEntry.svelte │ │ └── modals │ │ │ ├── create │ │ │ ├── CreateSingleValueTrackable.svelte │ │ │ └── CreateTrackableModal.svelte │ │ │ └── edit │ │ │ ├── EditTrackableImportExport.svelte │ │ │ ├── EditTrackableModal.svelte │ │ │ └── general │ │ │ ├── EditTrackableCard.svelte │ │ │ ├── EditTrackableCategory.svelte │ │ │ ├── EditTrackableGeneral.svelte │ │ │ ├── chart │ │ │ └── EditTrackableChartCard.svelte │ │ │ ├── tally │ │ │ └── EditTrackableTallyCard.svelte │ │ │ └── value │ │ │ ├── EditTrackableValueCard.svelte │ │ │ └── EditTrackableValueRepresentation.svelte │ └── variable │ │ └── edit │ │ ├── EditBackButton.svelte │ │ ├── EditConstant.svelte │ │ ├── EditConstantOrVariable.svelte │ │ ├── EditVariable.svelte │ │ ├── EditVariableName.svelte │ │ ├── aggregation │ │ ├── EditAggregationVariable.svelte │ │ ├── EditListFilter.svelte │ │ ├── EditListFilters.svelte │ │ └── EditListOperatorValue.svelte │ │ ├── calculation │ │ └── EditCalculationVariable.svelte │ │ └── latest │ │ └── EditLatestVariable.svelte ├── db │ ├── collections.ts │ ├── dexie │ │ ├── analytics.ts │ │ ├── dashboard.ts │ │ ├── db.ts │ │ ├── form.ts │ │ ├── goal.ts │ │ ├── index.ts │ │ ├── journal.ts │ │ ├── migration.ts │ │ ├── notification.ts │ │ ├── reflection.ts │ │ ├── search.ts │ │ ├── tag.ts │ │ ├── trackable.ts │ │ └── variable.ts │ └── migration │ │ ├── migration.ts │ │ └── migrations │ │ ├── chartTitles.ts │ │ └── defaultQuestionValues.ts ├── gridstack-extra-columns.css ├── main.ts ├── model │ ├── analytics │ │ ├── analytics.ts │ │ └── ui.ts │ ├── dashboard │ │ ├── dashboard.ts │ │ ├── suggestions.ts │ │ ├── ui.ts │ │ └── widgets │ │ │ ├── chart.ts │ │ │ ├── checklist.ts │ │ │ ├── entryRow.ts │ │ │ ├── goal.ts │ │ │ ├── insights.ts │ │ │ ├── metric.ts │ │ │ ├── newCorrelations.ts │ │ │ ├── table.ts │ │ │ ├── tags.ts │ │ │ ├── trackable.ts │ │ │ └── welcome.ts │ ├── form │ │ ├── data.ts │ │ ├── data │ │ │ ├── boolean.ts │ │ │ ├── date-time.ts │ │ │ ├── date.ts │ │ │ ├── hierarchy.ts │ │ │ ├── number.ts │ │ │ ├── rich-text.ts │ │ │ ├── text.ts │ │ │ ├── time-elapsed.ts │ │ │ └── time-of-day.ts │ │ ├── display.ts │ │ ├── display │ │ │ ├── hierarchy.ts │ │ │ ├── input.ts │ │ │ ├── range.ts │ │ │ ├── rich-input.ts │ │ │ ├── segmented.ts │ │ │ ├── select.ts │ │ │ └── text-area.ts │ │ ├── form.ts │ │ ├── suggestions.ts │ │ ├── ui.ts │ │ └── validation.ts │ ├── goal │ │ ├── goal.ts │ │ ├── suggestions.ts │ │ └── ui.ts │ ├── journal │ │ ├── journal.ts │ │ └── search │ │ │ ├── date.ts │ │ │ ├── freeText.ts │ │ │ ├── search.ts │ │ │ ├── tag.ts │ │ │ ├── trackable.ts │ │ │ └── ui.ts │ ├── notification │ │ └── notification.ts │ ├── onboarding │ │ └── onboarding.ts │ ├── primitive │ │ └── primitive.ts │ ├── reflection │ │ ├── reflection.ts │ │ ├── suggestions.ts │ │ ├── ui.ts │ │ └── widgets │ │ │ ├── checklist.ts │ │ │ ├── form.ts │ │ │ ├── table.ts │ │ │ └── tags.ts │ ├── sharedWidgets │ │ ├── checklist │ │ │ └── checklist.ts │ │ └── table │ │ │ └── table.ts │ ├── tag │ │ ├── suggestions.ts │ │ └── tag.ts │ ├── trackable │ │ ├── suggestions.ts │ │ ├── trackable.ts │ │ └── ui.ts │ ├── ui │ │ ├── button.ts │ │ ├── context-menu.ts │ │ ├── dropdown.ts │ │ ├── dynamicInput.ts │ │ ├── modal.ts │ │ ├── router.svelte.ts │ │ ├── segmented.ts │ │ └── sidebar.ts │ └── variable │ │ ├── time │ │ ├── serialization.ts │ │ └── time.ts │ │ ├── ui.ts │ │ └── variable.ts ├── services.ts ├── services │ ├── analytics │ │ ├── analytics.ts │ │ ├── display.ts │ │ ├── history.ts │ │ ├── ignore.ts │ │ └── settings.ts │ ├── dashboard │ │ ├── dashboard.ts │ │ └── widget.ts │ ├── deletion │ │ └── deletion.ts │ ├── export │ │ ├── complete │ │ │ └── complete.ts │ │ └── formEntries │ │ │ └── export.ts │ ├── form │ │ ├── form.ts │ │ └── template.ts │ ├── goal │ │ └── goal.ts │ ├── import │ │ ├── complete │ │ │ ├── complete.ts │ │ │ └── oldFormat.ts │ │ └── formEntries │ │ │ ├── csv.ts │ │ │ ├── import.ts │ │ │ └── json.ts │ ├── journal │ │ ├── journal.ts │ │ └── search.ts │ ├── notification │ │ ├── native.ts │ │ ├── notification.ts │ │ └── web.ts │ ├── observer.ts │ ├── reflection │ │ └── reflection.ts │ ├── tag │ │ ├── category.ts │ │ ├── entry.ts │ │ └── tag.ts │ ├── trackable │ │ ├── category.ts │ │ └── trackable.ts │ └── variable │ │ ├── dependencies.ts │ │ ├── filtering.ts │ │ ├── graph.ts │ │ ├── types │ │ ├── aggregate.ts │ │ ├── calculation.ts │ │ ├── goal.ts │ │ ├── goalStreak.ts │ │ ├── group.ts │ │ ├── latest.ts │ │ ├── list.ts │ │ ├── serialization.ts │ │ └── tag.ts │ │ └── variable.ts ├── stores.ts ├── stores │ ├── analytics │ │ ├── analytics.ts │ │ ├── settings.ts │ │ ├── tags.ts │ │ └── trackable.ts │ ├── cached.ts │ ├── dashboard │ │ ├── dashboard.ts │ │ └── widget │ │ │ ├── chart.ts │ │ │ ├── entryRow.ts │ │ │ ├── goal.ts │ │ │ ├── insights.ts │ │ │ ├── metric.ts │ │ │ └── trackable.ts │ ├── deletion │ │ └── deletion.ts │ ├── export │ │ ├── complete.ts │ │ └── formEntry.ts │ ├── form │ │ └── form.ts │ ├── goal │ │ ├── goal.ts │ │ └── value.ts │ ├── import │ │ ├── complete.ts │ │ └── formEntry.ts │ ├── journal │ │ ├── entry.ts │ │ ├── grouped.ts │ │ ├── search.ts │ │ └── tag.ts │ ├── onboarding │ │ └── onboarding.ts │ ├── reflection │ │ └── reflection.ts │ ├── sharedWidgets │ │ ├── checklist │ │ │ └── checklist.ts │ │ └── table │ │ │ └── table.ts │ ├── store.ts │ ├── tag │ │ ├── categorized.ts │ │ ├── category.ts │ │ ├── tag.ts │ │ └── value.ts │ ├── trackable │ │ ├── categorized.ts │ │ ├── category.ts │ │ ├── trackable.ts │ │ └── value.ts │ ├── ui │ │ ├── drawer.ts │ │ └── weekStart.ts │ └── variable │ │ ├── edit.ts │ │ ├── editState.ts │ │ ├── value.ts │ │ └── variable.ts ├── swSetup.ts ├── util │ ├── array.ts │ ├── category.ts │ ├── color.ts │ ├── event.ts │ ├── file.ts │ ├── local.ts │ ├── long-press.ts │ ├── math.ts │ ├── perf.ts │ ├── promise.ts │ ├── time │ │ ├── format.ts │ │ └── simple.ts │ └── window.ts ├── views │ ├── analytics │ │ ├── AnalyticsDetailView.svelte │ │ └── AnalyticsView.svelte │ ├── dashboard │ │ └── DashboardView.svelte │ ├── form │ │ └── FormEditorView.svelte │ ├── goal │ │ ├── GoalEditorView.svelte │ │ └── GoalView.svelte │ ├── journal │ │ ├── JournalSearchView.svelte │ │ └── JournalView.svelte │ ├── onboarding │ │ └── OnboardingView.svelte │ ├── reflection │ │ ├── ReflectionEditorView.svelte │ │ └── ReflectionListView.svelte │ ├── settings │ │ └── SettingsView.svelte │ ├── tag │ │ └── TagsView.svelte │ └── trackable │ │ └── TrackableView.svelte └── vite-env.d.ts ├── svelte.config.js ├── tailwind.config.js ├── tests ├── analytics │ ├── basic.test.ts │ ├── correlation.test.ts │ ├── history.test.ts │ ├── insights.test.ts │ ├── raw.test.ts │ ├── tags.test.ts │ └── weekday.test.ts ├── common.ts ├── dummy-collections.ts ├── graph │ ├── edit.test.ts │ ├── filter.test.ts │ ├── goal.test.ts │ ├── goalStreak.test.ts │ ├── graph.test.ts │ ├── group.test.ts │ ├── latest.test.ts │ └── tag.test.ts ├── journal │ └── search.test.ts ├── primitive.test.ts ├── simple-time.test.ts └── time-scope.test.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | Perfice.drawio 27 | .$Perfice.drawio.bkp 28 | 29 | dev-dist 30 | deploy-apk.sh 31 | deploy.sh -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["svelte.svelte-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # perfice 2 | Open source self-tracking app in Svelte 5. 3 | -------------------------------------------------------------------------------- /android/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build/* 2 | !/build/.npmkeep 3 | -------------------------------------------------------------------------------- /android/app/capacitor.build.gradle: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN 2 | 3 | android { 4 | compileOptions { 5 | sourceCompatibility JavaVersion.VERSION_21 6 | targetCompatibility JavaVersion.VERSION_21 7 | } 8 | } 9 | 10 | apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" 11 | dependencies { 12 | implementation project(':capacitor-app') 13 | implementation project(':capacitor-filesystem') 14 | implementation project(':capacitor-local-notifications') 15 | implementation project(':capacitor-share') 16 | 17 | } 18 | 19 | 20 | if (hasProperty('postBuildExtras')) { 21 | postBuildExtras() 22 | } 23 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.getcapacitor.myapp; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import android.content.Context; 6 | import androidx.test.ext.junit.runners.AndroidJUnit4; 7 | import androidx.test.platform.app.InstrumentationRegistry; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * @see Testing documentation 15 | */ 16 | @RunWith(AndroidJUnit4.class) 17 | public class ExampleInstrumentedTest { 18 | 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 23 | 24 | assertEquals("com.getcapacitor.app", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /android/app/src/main/java/dev/adoe/perfice/MainActivity.java: -------------------------------------------------------------------------------- 1 | package io.perfice.app; 2 | 3 | import android.os.Bundle; 4 | import android.webkit.WebView; 5 | 6 | import com.getcapacitor.BridgeActivity; 7 | 8 | public class MainActivity extends BridgeActivity { 9 | @Override 10 | public void onCreate(Bundle savedInstanceState) { 11 | super.onCreate(savedInstanceState); 12 | } 13 | 14 | @Override 15 | public void onStart() { 16 | super.onStart(); 17 | WebView webview = getBridge().getWebView(); 18 | webview.setOverScrollMode(WebView.OVER_SCROLL_NEVER); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-land-hdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/drawable-land-hdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-land-ldpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/drawable-land-ldpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-land-mdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/drawable-land-mdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-land-night-hdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/drawable-land-night-hdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-land-night-ldpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/drawable-land-night-ldpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-land-night-mdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/drawable-land-night-mdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-land-night-xhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/drawable-land-night-xhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-land-night-xxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/drawable-land-night-xxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-land-night-xxxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/drawable-land-night-xxxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-land-xhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/drawable-land-xhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-land-xxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/drawable-land-xxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-land-xxxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/drawable-land-xxxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-night/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/drawable-night/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-port-hdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/drawable-port-hdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-port-ldpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/drawable-port-ldpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-port-mdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/drawable-port-mdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-port-night-hdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/drawable-port-night-hdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-port-night-ldpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/drawable-port-night-ldpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-port-night-mdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/drawable-port-night-mdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-port-night-xhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/drawable-port-night-xhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-port-night-xxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/drawable-port-night-xxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-port-night-xxxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/drawable-port-night-xxxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-port-xhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/drawable-port-xhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-port-xxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/drawable-port-xxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-port-xxxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/drawable-port-xxxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/small_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/drawable/small_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/drawable/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-ldpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/mipmap-ldpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-ldpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/mipmap-ldpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-ldpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/mipmap-ldpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-ldpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/mipmap-ldpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Perfice 4 | Perfice 5 | io.perfice.app 6 | io.perfice.app 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 17 | 18 | 19 | 22 | -------------------------------------------------------------------------------- /android/app/src/main/res/xml/file_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.getcapacitor.myapp; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import org.junit.Test; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | 14 | @Test 15 | public void addition_isCorrect() throws Exception { 16 | assertEquals(4, 2 + 2); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | 5 | repositories { 6 | google() 7 | mavenCentral() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:8.7.2' 11 | classpath 'com.google.gms:google-services:4.4.2' 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | apply from: "variables.gradle" 19 | 20 | allprojects { 21 | repositories { 22 | google() 23 | mavenCentral() 24 | } 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /android/capacitor.settings.gradle: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN 2 | include ':capacitor-android' 3 | project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') 4 | 5 | include ':capacitor-app' 6 | project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android') 7 | 8 | include ':capacitor-filesystem' 9 | project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacitor/filesystem/android') 10 | 11 | include ':capacitor-local-notifications' 12 | project(':capacitor-local-notifications').projectDir = new File('../node_modules/@capacitor/local-notifications/android') 13 | 14 | include ':capacitor-share' 15 | project(':capacitor-share').projectDir = new File('../node_modules/@capacitor/share/android') 16 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | 19 | # AndroidX package structure to make it clearer which packages are bundled with the 20 | # Android operating system, and which are packaged with your app's APK 21 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 22 | android.useAndroidX=true 23 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | include ':capacitor-cordova-android-plugins' 3 | project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/') 4 | 5 | apply from: 'capacitor.settings.gradle' -------------------------------------------------------------------------------- /android/variables.gradle: -------------------------------------------------------------------------------- 1 | ext { 2 | minSdkVersion = 23 3 | compileSdkVersion = 35 4 | targetSdkVersion = 35 5 | androidxActivityVersion = '1.9.2' 6 | androidxAppCompatVersion = '1.7.0' 7 | androidxCoordinatorLayoutVersion = '1.2.0' 8 | androidxCoreVersion = '1.15.0' 9 | androidxFragmentVersion = '1.8.4' 10 | coreSplashScreenVersion = '1.0.1' 11 | androidxWebkitVersion = '1.12.1' 12 | junitVersion = '4.13.2' 13 | androidxJunitVersion = '1.2.1' 14 | androidxEspressoCoreVersion = '3.6.1' 15 | cordovaAndroidVersion = '10.1.1' 16 | } -------------------------------------------------------------------------------- /assets/icon-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/assets/icon-background.png -------------------------------------------------------------------------------- /assets/icon-foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/assets/icon-foreground.png -------------------------------------------------------------------------------- /assets/icon-only.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/assets/icon-only.png -------------------------------------------------------------------------------- /assets/splash-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/assets/splash-dark.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/assets/splash.png -------------------------------------------------------------------------------- /capacitor.config.ts: -------------------------------------------------------------------------------- 1 | import type {CapacitorConfig} from '@capacitor/cli'; 2 | 3 | const config: CapacitorConfig = { 4 | appId: 'io.perfice.app', 5 | appName: 'Perfice', 6 | webDir: 'dist', 7 | plugins: { 8 | LocalNotifications: { 9 | smallIcon: "res://drawable/small_icon", 10 | largeIcon: "res://drawable/splash", 11 | iconColor: "#16A34A" 12 | } 13 | } 14 | }; 15 | 16 | export default config; 17 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | Perfice 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/analytics-onboarding-mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/public/analytics-onboarding-mobile.png -------------------------------------------------------------------------------- /public/analytics-onboarding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/public/analytics-onboarding.png -------------------------------------------------------------------------------- /public/dashboard-onboarding-mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/public/dashboard-onboarding-mobile.png -------------------------------------------------------------------------------- /public/dashboard-onboarding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/public/dashboard-onboarding.png -------------------------------------------------------------------------------- /public/favicon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/public/favicon-128x128.png -------------------------------------------------------------------------------- /public/favicon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/public/favicon-512x512.png -------------------------------------------------------------------------------- /public/favicon-64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/public/favicon-64x64.png -------------------------------------------------------------------------------- /public/goals-onboarding-mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/public/goals-onboarding-mobile.png -------------------------------------------------------------------------------- /public/goals-onboarding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/public/goals-onboarding.png -------------------------------------------------------------------------------- /public/sw.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/public/sw.js -------------------------------------------------------------------------------- /public/tags-onboarding-mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/public/tags-onboarding-mobile.png -------------------------------------------------------------------------------- /public/tags-onboarding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/public/tags-onboarding.png -------------------------------------------------------------------------------- /public/trackables-onboarding-mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/public/trackables-onboarding-mobile.png -------------------------------------------------------------------------------- /public/trackables-onboarding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/public/trackables-onboarding.png -------------------------------------------------------------------------------- /src/assets/OpenMoji-black-glyf.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/591c0e4256dee64e1f1960e37bcf9ea53c47e393/src/assets/OpenMoji-black-glyf.woff2 -------------------------------------------------------------------------------- /src/assets/goal_suggestions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Daily steps", 4 | "color": "#ff0000", 5 | "conditions": [ 6 | { 7 | "type": "COMPARISON", 8 | "value": { 9 | "source": "steps", 10 | "operator": "GREATER_THAN_EQUAL", 11 | "target": 5000 12 | } 13 | } 14 | ] 15 | }, 16 | { 17 | "name": "Good sleep", 18 | "color": "#ff0000", 19 | "conditions": [ 20 | { 21 | "type": "COMPARISON", 22 | "value": { 23 | "source": "steps", 24 | "operator": "GREATER_THAN_EQUAL", 25 | "target": 5000 26 | } 27 | } 28 | ] 29 | } 30 | ] -------------------------------------------------------------------------------- /src/components/analytics/QuestionLabel.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 20 | 25 | -------------------------------------------------------------------------------- /src/components/analytics/details/CorrelationBar.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | {#if !full && positive} 10 |
11 | {/if} 12 |
13 |
14 |
15 |
21 |
22 |
23 |
24 | 25 | {#if !full && negative} 26 |
27 | {/if} 28 |
29 | -------------------------------------------------------------------------------- /src/components/analytics/details/CorrelationMessage.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |

9 | 10 | {display.between} 11 | 12 |

-------------------------------------------------------------------------------- /src/components/analytics/details/tag/TagWeekDayAnalytics.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 |

Week days

11 |

12 | Most tagged on {WEEK_DAYS_SHORT[analytics.max]}, least tagged on {WEEK_DAYS_SHORT[analytics.min]} 13 |

14 |
15 | 24 |
25 |
26 | -------------------------------------------------------------------------------- /src/components/analytics/details/trackable/BasicCategoricalAnalyticsRow.svelte: -------------------------------------------------------------------------------- 1 | 8 | 14 | 20 | -------------------------------------------------------------------------------- /src/components/analytics/tag/AnalyticsTagView.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | {#await $res} 11 | Loading... 12 | {:then data} 13 | {#each data.results as value(value.tag.id)} 14 |
15 |

{value.tag.name}

17 |
18 | 19 |
20 |
21 | {/each} 22 | {/await} 23 |
24 | -------------------------------------------------------------------------------- /src/components/analytics/trackable/AnalyticsTrackableLineChart.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 22 | -------------------------------------------------------------------------------- /src/components/base/button/Button.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 22 | -------------------------------------------------------------------------------- /src/components/base/button/CardButton.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/components/base/button/HorizontalPlusButton.svelte: -------------------------------------------------------------------------------- 1 | 8 | 14 | -------------------------------------------------------------------------------- /src/components/base/button/IconButton.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /src/components/base/button/LineButton.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /src/components/base/button/PopupIconButton.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/components/base/button/SelectCardButton.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 | 32 | -------------------------------------------------------------------------------- /src/components/base/calendarScroll/CalendarScrollItem.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | -------------------------------------------------------------------------------- /src/components/base/card/GenericActionsCard.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 |
16 | {#if icon != null} 17 | 18 | {/if} 19 | 20 | {text} 21 |
22 |
23 | {@render actions()} 24 |
25 |
26 | -------------------------------------------------------------------------------- /src/components/base/card/GenericEditDeleteCard.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | {#snippet actions()} 18 | 19 | 20 | {/snippet} 21 | -------------------------------------------------------------------------------- /src/components/base/card/TitledCard.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 |
18 |
19 | 20 | {#if icon != null} 21 | 22 | {/if} 23 | 24 |
25 |

{title}

26 |

{description}

27 |
28 |
29 |
30 | {@render suffix?.()} 31 |
32 |
33 | -------------------------------------------------------------------------------- /src/components/base/color/ColorPickerButton.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | onChange?.(e.currentTarget.value)}> 7 | -------------------------------------------------------------------------------- /src/components/base/contextMenu/ContextMenuButtons.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | {#each buttons as button} 11 | 18 | {/each} 19 |
20 | -------------------------------------------------------------------------------- /src/components/base/contextMenu/PopupContextMenuButton.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/components/base/dropdown/BindableDropdownButton.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | { 20 | return { 21 | ...i, 22 | action: () => change(i.value), 23 | } 24 | })}/> 25 | -------------------------------------------------------------------------------- /src/components/base/dynamic/DynamicLabel.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | {@render children()} 9 | 10 | -------------------------------------------------------------------------------- /src/components/base/fileButton/FileButton.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 | 33 | 34 | -------------------------------------------------------------------------------- /src/components/base/gesture/SwipeDetector.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 | -------------------------------------------------------------------------------- /src/components/base/icon/Icon.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | {name !== "" ? name : "\u2b50\ufe0f"} 7 | 8 | -------------------------------------------------------------------------------- /src/components/base/iconLabel/IconLabel.svelte: -------------------------------------------------------------------------------- 1 | 8 |
9 | 10 |

{title}

11 |
12 | -------------------------------------------------------------------------------- /src/components/base/iconLabel/IconLabelBetween.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | 11 | 12 | {@render children()} 13 |
14 | -------------------------------------------------------------------------------- /src/components/base/inline/InlineCreateInput.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 | 26 | -------------------------------------------------------------------------------- /src/components/base/inline/InlineCreateLineButton.svelte: -------------------------------------------------------------------------------- 1 | 15 | {#if addingCategory} 16 |
17 | addingCategory = false} 18 | onSubmit={(name) => onSubmit(name)}/> 19 |
20 | {:else} 21 | 22 | {/if} 23 | -------------------------------------------------------------------------------- /src/components/base/invertedSegmented/InvertedSegment.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | 18 | 27 | -------------------------------------------------------------------------------- /src/components/base/invertedSegmented/InvertedSegmentedControl.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 |
23 | {#each segments as segment} 24 | onSegmentClick(segment)}> 26 | 27 | {#if segment.prefix != null} 28 | 29 | {/if} 30 | {segment.name} 31 | {#if segment.suffix != null} 32 | 33 | {/if} 34 | 35 | {/each} 36 |
37 | -------------------------------------------------------------------------------- /src/components/base/modal/ModalFooterContainer.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /src/components/base/modal/generic/GenericDeleteModal.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | -------------------------------------------------------------------------------- /src/components/base/modal/generic/GenericEntityModal.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 | 30 | {message} 31 | 32 | -------------------------------------------------------------------------------- /src/components/base/segmented/Segment.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 28 | 29 | 46 | -------------------------------------------------------------------------------- /src/components/base/timeScope/RangedTimeScopePicker.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 |
21 | 22 | 23 |
24 | -------------------------------------------------------------------------------- /src/components/base/timeScope/SimpleTimeScopePicker.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /src/components/base/title/Title.svelte: -------------------------------------------------------------------------------- 1 | 11 | 15 | -------------------------------------------------------------------------------- /src/components/base/title/TitleAndCalendar.svelte: -------------------------------------------------------------------------------- 1 | 13 |
14 | 15 | <CalendarScroll value={date} onChange={onDateChange}/> 16 | </div> 17 | -------------------------------------------------------------------------------- /src/components/base/weekDays/WeekDays.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import {createDefaultWeekDays} from "@perfice/services/variable/types/goalStreak"; 3 | import {WEEK_DAYS_SHORT} from "@perfice/util/time/format"; 4 | 5 | let {value, onChange}: { value: number[] | null, onChange: (weekDays: number[]) => void } = $props(); 6 | 7 | function onWeekDayChange(index: number) { 8 | if (value == null) { 9 | onChange(createDefaultWeekDays().filter(v => v != index)); 10 | return; 11 | } 12 | 13 | let checked = value.includes(index); 14 | if (checked) { 15 | onChange(value.filter(v => v != index)); 16 | } else { 17 | onChange([...value, index]); 18 | } 19 | } 20 | </script> 21 | 22 | <div class="flex gap-2 items-center"> 23 | {#each Array(7) as _, i} 24 | <div class="flex flex-col items-center"> 25 | <input type="checkbox" checked={value == null || value.includes(i)} 26 | onchange={() => onWeekDayChange(i)}/> 27 | {WEEK_DAYS_SHORT[i][0]} 28 | </div> 29 | {/each} 30 | </div> -------------------------------------------------------------------------------- /src/components/chart/CanvasChartRenderer.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import {Chart, type ChartData, type ChartType, type DefaultDataPoint} from 'chart.js'; 3 | import 'chart.js/auto'; 4 | import {onMount} from "svelte"; 5 | import type {ChartConfiguration} from "chart.js"; 6 | 7 | const { 8 | config, 9 | data, 10 | }: { 11 | config: ChartConfiguration<ChartType, DefaultDataPoint<ChartType>>, 12 | data: ChartData<ChartType, DefaultDataPoint<ChartType>> 13 | } = $props(); 14 | 15 | let canvasElem: HTMLCanvasElement; 16 | let chart: Chart; 17 | 18 | onMount(() => { 19 | chart = new Chart(canvasElem, config); 20 | 21 | return () => { 22 | chart.destroy(); 23 | }; 24 | }); 25 | 26 | $effect(() => { 27 | if (chart) { 28 | chart.data = data; 29 | chart.update(); 30 | } 31 | }); 32 | </script> 33 | 34 | <canvas class="rounded-b-xl" bind:this={canvasElem}></canvas> 35 | -------------------------------------------------------------------------------- /src/components/dashboard/sidebar/add/AddWidgetSidebar.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {DashboardAddWidgetAction} from "@perfice/model/dashboard/ui"; 3 | import {getDashboardWidgetDefinitions} from "@perfice/model/dashboard/dashboard"; 4 | import DashboardDragInCard from "@perfice/components/dashboard/sidebar/add/DashboardDragInCard.svelte"; 5 | 6 | let {action}: { action: DashboardAddWidgetAction } = $props(); 7 | 8 | let definitions = getDashboardWidgetDefinitions(); 9 | let mobile = window.innerWidth < 768; 10 | </script> 11 | 12 | {#if mobile} 13 | <p class="text-sm mb-4">The new widget will be added to the bottom of the dashboard.</p> 14 | {/if} 15 | 16 | <div class="grid grid-cols-2 gap-2"> 17 | {#each definitions as definition} 18 | <DashboardDragInCard {definition} {mobile} onClick={() => action.onClick(definition.getType(), mobile)}/> 19 | {/each} 20 | </div> -------------------------------------------------------------------------------- /src/components/dashboard/sidebar/edit/types/checklist/EditChecklistWidgetSidebar.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {Form} from "@perfice/model/form/form"; 3 | import type { 4 | DashboardChecklistWidgetSettings 5 | } from "@perfice/model/dashboard/widgets/checklist"; 6 | import EditChecklistWidgetSettings 7 | from "@perfice/components/sharedWidgets/checklist/EditChecklistWidgetSettings.svelte"; 8 | 9 | let {settings, onChange, forms}: { 10 | settings: DashboardChecklistWidgetSettings, 11 | onChange: (settings: DashboardChecklistWidgetSettings) => void, 12 | forms: Form[], 13 | dependencies: Record<string, string> 14 | } = $props(); 15 | </script> 16 | 17 | <EditChecklistWidgetSettings {settings} {onChange} {forms}/> -------------------------------------------------------------------------------- /src/components/dashboard/sidebar/edit/types/entryRow/EditEntryRowWidgetSidebar.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {Form} from "@perfice/model/form/form"; 3 | import type {DashboardEntryRowWidgetSettings} from "@perfice/model/dashboard/widgets/entryRow"; 4 | import SelectFormAndQuestion from "@perfice/components/dashboard/sidebar/edit/SelectFormAndQuestion.svelte"; 5 | 6 | let {settings, onChange, forms}: { 7 | settings: DashboardEntryRowWidgetSettings, 8 | onChange: (settings: DashboardEntryRowWidgetSettings) => void, 9 | forms: Form[], 10 | dependencies: Record<string, string> 11 | } = $props(); 12 | </script> 13 | 14 | <SelectFormAndQuestion onChange={onChange} settings={settings} forms={forms}/> 15 | -------------------------------------------------------------------------------- /src/components/dashboard/sidebar/edit/types/insights/EditInsightsWidgetSidebar.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {DashboardInsightsWidgetSettings} from "@perfice/model/dashboard/widgets/insights"; 3 | import type {Form} from "@perfice/model/form/form"; 4 | import {SIMPLE_TIME_SCOPE_TYPES} from "@perfice/model/variable/ui"; 5 | import BindableDropdownButton from "@perfice/components/base/dropdown/BindableDropdownButton.svelte"; 6 | 7 | let {settings, onChange}: { 8 | settings: DashboardInsightsWidgetSettings, 9 | onChange: (settings: DashboardInsightsWidgetSettings) => void, 10 | forms: Form[], 11 | dependencies: Record<string, string> 12 | } = $props(); 13 | 14 | </script> 15 | 16 | <div class="row-between mt-2"> 17 | Time scope 18 | <BindableDropdownButton value={settings.timeScope} 19 | onChange={(v) => onChange({...settings, timeScope: v})} 20 | items={SIMPLE_TIME_SCOPE_TYPES}/> 21 | </div> -------------------------------------------------------------------------------- /src/components/dashboard/sidebar/edit/types/newCorrelations/EditNewCorrelationsWidgetSidebar.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {Form} from "@perfice/model/form/form"; 3 | import type {DashboardNewCorrelationsWidgetSettings} from "@perfice/model/dashboard/widgets/newCorrelations"; 4 | 5 | let {settings, onChange, forms}: { 6 | settings: DashboardNewCorrelationsWidgetSettings, 7 | onChange: (settings: DashboardNewCorrelationsWidgetSettings) => void, 8 | forms: Form[], 9 | dependencies: Record<string, string> 10 | } = $props(); 11 | </script> 12 | -------------------------------------------------------------------------------- /src/components/dashboard/sidebar/edit/types/table/EditTableWidgetSidebar.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import EditTableWidgetSettings from "@perfice/components/sharedWidgets/table/EditTableWidgetSettings.svelte"; 3 | import type {TableWidgetSettings} from "@perfice/model/sharedWidgets/table/table.js"; 4 | import type {Form} from "@perfice/model/form/form.js"; 5 | import type {DashboardTableWidgetSettings} from "@perfice/model/dashboard/widgets/table"; 6 | 7 | let {settings, onChange, forms}: { 8 | settings: DashboardTableWidgetSettings, 9 | onChange: (settings: DashboardTableWidgetSettings) => void, 10 | forms: Form[], 11 | dependencies: Record<string, string> 12 | } = $props(); 13 | </script> 14 | 15 | <EditTableWidgetSettings {settings} {onChange} {forms}/> -------------------------------------------------------------------------------- /src/components/dashboard/sidebar/edit/types/trackable/EditTrackableWidgetSidebar.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {DashboardTrackableWidgetSettings} from "@perfice/model/dashboard/widgets/trackable"; 3 | import type {Form} from "@perfice/model/form/form"; 4 | import BindableDropdownButton from "@perfice/components/base/dropdown/BindableDropdownButton.svelte"; 5 | import {trackables} from "@perfice/stores"; 6 | 7 | let {settings, onChange, forms}: { 8 | settings: DashboardTrackableWidgetSettings, 9 | onChange: (settings: DashboardTrackableWidgetSettings) => void, 10 | forms: Form[], 11 | dependencies: Record<string, string> 12 | } = $props(); 13 | 14 | 15 | let availableTrackables = $derived.by(async () => (await trackables.fetchTrackables()).map(v => { 16 | return {value: v.id, name: v.name} 17 | })); 18 | 19 | function onTrackableChange(trackableId: string) { 20 | onChange({...settings, trackableId: trackableId}); 21 | } 22 | </script> 23 | {#await availableTrackables} 24 | Loading... 25 | {:then value} 26 | <div class="row-between"> 27 | Trackable 28 | <BindableDropdownButton value={settings.trackableId} items={value} 29 | onChange={onTrackableChange}/> 30 | 31 | </div> 32 | {/await} 33 | -------------------------------------------------------------------------------- /src/components/dashboard/sidebar/edit/types/welcome/EditWelcomeWidgetSidebar.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {DashboardWelcomeWidgetSettings} from "@perfice/model/dashboard/widgets/welcome"; 3 | import type {Form} from "@perfice/model/form/form"; 4 | 5 | let {settings, onChange, forms}: { 6 | settings: DashboardWelcomeWidgetSettings, 7 | onChange: (settings: DashboardWelcomeWidgetSettings) => void, 8 | forms: Form[], 9 | dependencies: Record<string, string> 10 | } = $props(); 11 | </script> -------------------------------------------------------------------------------- /src/components/dashboard/types/checkList/ChecklistEntry.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import {faCheckCircle} from "@fortawesome/free-solid-svg-icons"; 3 | import {faCheckCircle as regularCheckCircle} from "@fortawesome/free-regular-svg-icons"; 4 | import IconButton from "@perfice/components/base/button/IconButton.svelte"; 5 | 6 | let {checked, name, onClick}: { checked: boolean, name: string, onClick: () => void } = $props(); 7 | </script> 8 | 9 | <div class="flex gap-1 items-center [&:not(:last-child)]:border-b px-1"> 10 | <IconButton icon={checked ? faCheckCircle : regularCheckCircle} 11 | {onClick} 12 | class="text-xl {checked ? 'text-green-500' : 'text-gray-500'}" 13 | /> 14 | <span class="text-sm">{name}</span> 15 | </div> 16 | -------------------------------------------------------------------------------- /src/components/dashboard/types/entryRow/EntryRowItem.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {EntryRowWidgetEntry} from "@perfice/stores/dashboard/widget/entryRow"; 3 | import Icon from "@perfice/components/base/icon/Icon.svelte"; 4 | import {formatTimestampHHMM} from "@perfice/util/time/format"; 5 | 6 | let {entry}: { entry: EntryRowWidgetEntry } = $props(); 7 | </script> 8 | 9 | <div class="p-2 flex flex-col items-center"> 10 | {#if entry.icon} 11 | <Icon name={entry.value} class="text-2xl"/> 12 | {:else} 13 | {entry.value} 14 | {/if} 15 | <span class="text-gray-400 text-sm">{formatTimestampHHMM(entry.timestamp)}</span> 16 | </div> 17 | -------------------------------------------------------------------------------- /src/components/dashboard/types/goal/DashboardGoalWidget.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {DashboardGoalWidgetSettings} from "@perfice/model/dashboard/widgets/goal"; 3 | import GoalCardBase from "@perfice/components/goal/GoalCardBase.svelte"; 4 | import {dashboardDate} from "@perfice/stores/dashboard/dashboard"; 5 | import {goalWidget, weekStart} from "@perfice/stores"; 6 | 7 | let {settings}: { 8 | settings: DashboardGoalWidgetSettings, 9 | dependencies: Record<string, string>, 10 | openFormModal: (formId: string) => void 11 | } = $props(); 12 | 13 | let res = $derived(goalWidget(settings, $dashboardDate, $weekStart, 14 | settings.goalVariableId + ":" + settings.goalStreakVariableId)); 15 | </script> 16 | 17 | <div 18 | class="border rounded-xl flex flex-col justify-center items-center w-full h-full bg-white" 19 | > 20 | {#await $res} 21 | Please select a goal 22 | {:then value} 23 | <GoalCardBase goal={value.goal} value={value.value} streak={value.value.streak}> 24 | </GoalCardBase> 25 | {/await} 26 | </div> 27 | -------------------------------------------------------------------------------- /src/components/dashboard/types/table/DashboardTableWidget.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {DashboardTableWidgetSettings} from "@perfice/model/dashboard/widgets/table"; 3 | import {dashboardDate} from "@perfice/stores/dashboard/dashboard"; 4 | import {type PrimitiveValue} from "@perfice/model/primitive/primitive"; 5 | import TableWidget from "@perfice/components/sharedWidgets/table/TableWidget.svelte"; 6 | 7 | let {dependencies, openFormModal, settings}: { 8 | settings: DashboardTableWidgetSettings, 9 | dependencies: Record<string, string>, 10 | openFormModal: (formId: string, answers?: Record<string, PrimitiveValue>) => void 11 | } = $props(); 12 | </script> 13 | 14 | <TableWidget {settings} date={$dashboardDate} {openFormModal} listVariableId={dependencies["list"]}/> 15 | -------------------------------------------------------------------------------- /src/components/dashboard/types/table/TableWidgetEntry.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {TableWidgetEntry} from "@perfice/stores/sharedWidgets/table/table"; 3 | 4 | let {entry}: { entry: TableWidgetEntry } = $props(); 5 | </script> 6 | 7 | <div class="w-full px-2 py-1 row-between [&:not(:last-child)]:border-b"> 8 | <p> 9 | {entry.prefix} 10 | </p> 11 | <p>{entry.suffix}</p> 12 | </div> -------------------------------------------------------------------------------- /src/components/dashboard/types/table/TableWidgetGroupHeader.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import IconButton from "@perfice/components/base/button/IconButton.svelte"; 3 | import {faPlus} from "@fortawesome/free-solid-svg-icons"; 4 | 5 | let {name, onLog}: { name: string, onLog: () => void } = $props(); 6 | </script> 7 | 8 | <div class="border-b w-full px-2 py-1 text-white bg-green-500 font-bold row-between"> 9 | {name} 10 | <IconButton icon={faPlus} onClick={onLog} class="pointer-feedback:bg-green-600"/> 11 | </div> 12 | -------------------------------------------------------------------------------- /src/components/dashboard/types/trackable/DashboardTrackableWidget.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {DashboardTrackableWidgetSettings} from "@perfice/model/dashboard/widgets/trackable"; 3 | import TrackableCard from "@perfice/components/trackable/card/TrackableCard.svelte"; 4 | import {dashboardDate} from "@perfice/stores/dashboard/dashboard"; 5 | import type {Trackable} from "@perfice/model/trackable/trackable"; 6 | import {trackableWidget, weekStart} from "@perfice/stores"; 7 | 8 | let {settings, openFormModal}: { 9 | settings: DashboardTrackableWidgetSettings, 10 | dependencies: Record<string, string>, 11 | openFormModal: (formId: string) => void 12 | } = $props(); 13 | 14 | let res = $derived(trackableWidget(settings)); 15 | 16 | function onLog(trackable: Trackable) { 17 | openFormModal(trackable.formId); 18 | } 19 | </script> 20 | 21 | {#await $res} 22 | Loading... 23 | {:then value} 24 | <TrackableCard class="max-h-none" trackable={value.trackable} date={$dashboardDate} 25 | weekStart={$weekStart} onEdit={() => {}} onLog={() => onLog(value.trackable)}/> 26 | {/await} -------------------------------------------------------------------------------- /src/components/dashboard/types/welcome/DashboardWelcomeWidget.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {DashboardWelcomeWidgetSettings} from "@perfice/model/dashboard/widgets/welcome"; 3 | import Fa from "svelte-fa"; 4 | import {faSun} from "@fortawesome/free-solid-svg-icons"; 5 | 6 | let {settings}: { 7 | settings: DashboardWelcomeWidgetSettings, 8 | dependencies: Record<string, string>, 9 | openFormModal: (formId: string) => void 10 | } = $props(); 11 | </script> 12 | 13 | <div class="p-4 md:p-6 w-full h-full box-border bg-gradient-to-r from-green-500 to-green-600 rounded-md shadow-md text-white flex flex-col justify-between"> 14 | <div><h2 class="text-3xl font-bold">Good morning!</h2> 15 | <p>Today is going to be a great day!</p></div> 16 | <div class="flex justify-end"> 17 | <Fa icon={faSun} class="text-7xl"/> 18 | </div> 19 | </div> 20 | -------------------------------------------------------------------------------- /src/components/form/editor/data/hierarchy/EditHierarchyQuestionSettings.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {HierarchyFormQuestionDataSettings, HierarchyOption} from "@perfice/model/form/data/hierarchy"; 3 | import EditHierarchyOption from "@perfice/components/form/editor/data/hierarchy/EditHierarchyOption.svelte"; 4 | import EditHierarchyOptionModal 5 | from "@perfice/components/form/editor/data/hierarchy/EditHierarchyOptionModal.svelte"; 6 | 7 | let editModal: EditHierarchyOptionModal; 8 | let {settings, onChange}: { 9 | settings: HierarchyFormQuestionDataSettings, 10 | onChange: (settings: HierarchyFormQuestionDataSettings) => void 11 | } = $props(); 12 | 13 | function onEdit(option: HierarchyOption) { 14 | editModal.open(option); 15 | } 16 | 17 | function onRootChange(option: HierarchyOption) { 18 | onChange({...settings, root: option}) 19 | } 20 | </script> 21 | 22 | <EditHierarchyOptionModal bind:this={editModal}/> 23 | <EditHierarchyOption option={settings.root} onChange={onRootChange} root={true} {onEdit} onDelete={() => {}}/> 24 | 25 | -------------------------------------------------------------------------------- /src/components/form/editor/display/hierarchy/EditHierarchyQuestionDisplaySettings.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {FormQuestionDataType} from "@perfice/model/form/form"; 3 | import type {HierarchyFormDisplaySettings} from "@perfice/model/form/display/hierarchy"; 4 | 5 | let {settings, onChange}: { 6 | settings: HierarchyFormDisplaySettings, 7 | onChange: (settings: HierarchyFormDisplaySettings) => void, 8 | dataType: FormQuestionDataType, 9 | dataSettings: any 10 | } = $props(); 11 | 12 | function onOnlyLeafOptionChange(e: { currentTarget: HTMLInputElement }) { 13 | onChange({...settings, onlyLeafOption: e.currentTarget.checked}); 14 | } 15 | </script> 16 | 17 | <div class="row-between"> 18 | Show only last option 19 | <input type="checkbox" checked={settings.onlyLeafOption} onchange={onOnlyLeafOptionChange}/> 20 | </div> 21 | -------------------------------------------------------------------------------- /src/components/form/editor/display/range/EditRangeQuestionSettings.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {FormQuestionDataType} from "@perfice/model/form/form"; 3 | import type {RangeFormQuestionSettings} from "@perfice/model/form/display/range"; 4 | 5 | let {settings, onChange, dataType, dataSettings}: { 6 | settings: RangeFormQuestionSettings, 7 | onChange: (settings: RangeFormQuestionSettings) => void, 8 | dataType: FormQuestionDataType, 9 | dataSettings: any 10 | } = $props(); 11 | 12 | function onStepChange(e: { currentTarget: HTMLInputElement }) { 13 | onChange({...settings, step: parseInt(e.currentTarget.value)}); 14 | } 15 | </script> 16 | 17 | <div class="row-between"> 18 | Step 19 | <input type="number" class="border" value={settings.step} onchange={onStepChange} min="1"/> 20 | </div> 21 | -------------------------------------------------------------------------------- /src/components/form/editor/display/segmented/EditSegmentedOptionCard.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import GenericEditDeleteCard from "@perfice/components/base/card/GenericEditDeleteCard.svelte"; 3 | import type {SegmentedOption} from "@perfice/model/form/display/segmented"; 4 | 5 | let {option, onDelete, onEdit}: { option: SegmentedOption, onDelete: () => void, onEdit: () => void } = $props(); 6 | </script> 7 | 8 | <GenericEditDeleteCard {onEdit} {onDelete} text={option.text}/> 9 | -------------------------------------------------------------------------------- /src/components/form/editor/display/select/EditSelectGrid.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {SelectGrid} from "@perfice/model/form/display/select"; 3 | 4 | let {grid, onChange}: { grid: SelectGrid, onChange: (grid: SelectGrid) => void } = $props(); 5 | 6 | function onItemsPerRowChange(e: { currentTarget: HTMLInputElement }) { 7 | onChange({...grid, itemsPerRow: parseInt(e.currentTarget.value)}); 8 | } 9 | 10 | function onBorderChange(e: { currentTarget: HTMLInputElement }) { 11 | onChange({...grid, border: e.currentTarget.checked}); 12 | } 13 | </script> 14 | 15 | <div class="mt-2 flex flex-col gap-2"> 16 | <div class="row-between"> 17 | <p>Items per row</p> 18 | <input type="number" class="border w-1/2" value={grid.itemsPerRow} onchange={onItemsPerRowChange}/> 19 | </div> 20 | <div class="row-between"> 21 | <p>Border</p> 22 | <input type="checkbox" class="border" checked={grid.border} onchange={onBorderChange}/> 23 | </div> 24 | </div> 25 | -------------------------------------------------------------------------------- /src/components/form/editor/display/select/EditSelectOptionCard.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {SelectOption} from "@perfice/model/form/display/select"; 3 | import GenericEditDeleteCard from "@perfice/components/base/card/GenericEditDeleteCard.svelte"; 4 | import {ICONS} from "@perfice/components/base/icon/icons"; 5 | 6 | let {option, onDelete, onEdit}: { option: SelectOption, onDelete: () => void, onEdit: () => void } = $props(); 7 | </script> 8 | 9 | <GenericEditDeleteCard {onEdit} {onDelete} icon={option.icon != null ? ICONS[option.icon] : undefined} 10 | text={option.text}/> 11 | 12 | -------------------------------------------------------------------------------- /src/components/form/editor/sidebar/SidebarDropdownHeader.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts" generics="T"> 2 | import {type IconDefinition} from "@fortawesome/free-solid-svg-icons"; 3 | // noinspection ES6UnusedImports 4 | import Fa from "svelte-fa"; 5 | import DropdownButton from "@perfice/components/base/dropdown/DropdownButton.svelte"; 6 | import type {DropdownMenuItem} from "@perfice/model/ui/dropdown"; 7 | 8 | 9 | let {icon, title, value, items}: { 10 | icon: IconDefinition, 11 | title: string, 12 | value: T, 13 | items: DropdownMenuItem<T>[] 14 | } = $props(); 15 | </script> 16 | 17 | <div class="mt-4 row-between bg-green-600 px-4 py-2"> 18 | <div class="row-gap font-bold text-white"> 19 | <Fa icon={icon} class="w-4"/> 20 | <p>{title}</p> 21 | </div> 22 | <DropdownButton class="min-w-40 bg-white text-black" value={value} items={items}/> 23 | </div> 24 | -------------------------------------------------------------------------------- /src/components/form/fields/hierarchy/HierarchyButton.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {HierarchyOption} from "@perfice/model/form/data/hierarchy"; 3 | import {sanitizeColor} from "@perfice/util/color"; 4 | 5 | let {option, onClick, selected}: { option: HierarchyOption, onClick: () => void, selected: boolean } = $props(); 6 | </script> 7 | 8 | <button class="flex-col-center aspect-square rounded-xl border-2 border-transparent " class:selected={selected} 9 | style:background-color={sanitizeColor(option.color)} 10 | onclick={onClick}> 11 | <p class="whitespace-pre-line text-center overflow-hidden text-ellipsis w-full text-sm md:text-base">{option.text}</p> 12 | </button> 13 | 14 | <style> 15 | .selected { 16 | @apply border-green-600; 17 | } 18 | </style> 19 | -------------------------------------------------------------------------------- /src/components/form/fields/input/BooleanInputFormField.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import SegmentedControl from "@perfice/components/base/segmented/SegmentedControl.svelte"; 3 | import type {InputFieldProps} from "@perfice/model/form/ui"; 4 | 5 | let {value, onChange, disabled}: InputFieldProps = $props(); 6 | 7 | const BOOLEAN_SEGMENTS = [ 8 | {name: "True", value: true}, 9 | {name: "False", value: false} 10 | ]; 11 | </script> 12 | 13 | <SegmentedControl {disabled} {value} inverted={false} segments={BOOLEAN_SEGMENTS} {onChange}/> 14 | -------------------------------------------------------------------------------- /src/components/form/fields/input/DateInputFormField.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {InputFieldProps} from "@perfice/model/form/ui"; 3 | import DatePicker from "@perfice/components/base/datePicker/DatePicker.svelte"; 4 | 5 | let {value, onChange, disabled}: InputFieldProps = $props(); 6 | 7 | </script> 8 | 9 | <DatePicker {value} {disabled} {onChange} /> 10 | -------------------------------------------------------------------------------- /src/components/form/fields/input/DateTimeInputFormField.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {InputFieldProps} from "@perfice/model/form/ui"; 3 | import DatePicker from "@perfice/components/base/datePicker/DatePicker.svelte"; 4 | 5 | let {value, onChange, disabled}: InputFieldProps = $props(); 6 | 7 | </script> 8 | 9 | <DatePicker time={true} {value} {disabled} {onChange} /> 10 | -------------------------------------------------------------------------------- /src/components/form/fields/input/TimeElapsedInputFormField.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {InputFieldProps} from "@perfice/model/form/ui"; 3 | import TimePicker from "@perfice/components/base/timePicker/TimePicker.svelte"; 4 | 5 | let {value, onChange, disabled}: InputFieldProps = $props(); 6 | 7 | </script> 8 | 9 | <TimePicker time={value} {onChange} {disabled} /> 10 | -------------------------------------------------------------------------------- /src/components/form/fields/input/TimeOfDayInputFormField.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {InputFieldProps} from "@perfice/model/form/ui"; 3 | import TimePicker from "@perfice/components/base/timePicker/TimePicker.svelte"; 4 | 5 | let {value, onChange, disabled}: InputFieldProps = $props(); 6 | </script> 7 | 8 | <TimePicker time={value} {onChange} {disabled} day={true} /> 9 | -------------------------------------------------------------------------------- /src/components/form/fields/input/VanillaInputFormField.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import {getHtmlInputFromQuestionType, type InputFieldProps} from "@perfice/model/form/ui"; 3 | 4 | let {dataType, disabled, value, onChange}: InputFieldProps = $props(); 5 | 6 | let input: HTMLInputElement; 7 | 8 | export function focus(){ 9 | input.focus(); 10 | } 11 | 12 | function onInputChange(e: { currentTarget: HTMLInputElement }) { 13 | onChange(e.currentTarget.value); 14 | } 15 | </script> 16 | 17 | <input class="border bg-white" {disabled} value={value} onchange={onInputChange} 18 | bind:this={input} 19 | type={getHtmlInputFromQuestionType(dataType)}/> 20 | -------------------------------------------------------------------------------- /src/components/form/fields/range/RangeFormField.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {RangeFormQuestionSettings} from "@perfice/model/form/display/range"; 3 | import type {FormFieldProps} from "@perfice/model/form/ui"; 4 | // noinspection ES6UnusedImports 5 | import RangeSlider from "svelte-range-slider-pips"; 6 | import type {NumberFormQuestionDataSettings} from "@perfice/model/form/data/number"; 7 | 8 | let {dataSettings, displaySettings, disabled, value, onChange}: FormFieldProps = $props(); 9 | 10 | let data = $derived(dataSettings as NumberFormQuestionDataSettings); 11 | let display = $derived(displaySettings as RangeFormQuestionSettings); 12 | </script> 13 | 14 | <RangeSlider on:change={(e) => onChange(e.detail.value)} 15 | pips 16 | springValues={{ stiffness: 1.0, damping: 1.0 }} 17 | all="label" 18 | value={value} min={data.min ?? 0} max={data.max ?? 100} step={display.step ?? 1} {disabled}/> -------------------------------------------------------------------------------- /src/components/form/fields/richInput/RichInputFormField.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {RichInputFormQuestionSettings} from "@perfice/model/form/display/rich-input"; 3 | import type {FormFieldProps} from "@perfice/model/form/ui"; 4 | 5 | let {dataSettings, displaySettings, disabled, value, onChange}: FormFieldProps = $props(); 6 | 7 | let display = displaySettings as RichInputFormQuestionSettings; 8 | </script> 9 | -------------------------------------------------------------------------------- /src/components/form/fields/segmented/SegmentedFormField.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {SegmentedFormQuestionSettings} from "@perfice/model/form/display/segmented"; 3 | import type {FormFieldProps} from "@perfice/model/form/ui"; 4 | import SegmentedControl from "@perfice/components/base/segmented/SegmentedControl.svelte"; 5 | 6 | let {displaySettings, value, onChange}: FormFieldProps = $props(); 7 | 8 | let display = $derived(displaySettings as SegmentedFormQuestionSettings); 9 | </script> 10 | 11 | <SegmentedControl {value} 12 | segments={display.options.map(o => { 13 | return { 14 | name: o.text, 15 | value: o.value.value, 16 | } 17 | })} {onChange}/> 18 | -------------------------------------------------------------------------------- /src/components/form/fields/select/SelectOptionButton.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import Icon from "@perfice/components/base/icon/Icon.svelte"; 3 | import type {SelectOption} from "@perfice/model/form/display/select"; 4 | 5 | let {option, selected, onClick, grid, unit}: { 6 | option: SelectOption, 7 | selected: boolean, 8 | onClick: () => void, 9 | grid: boolean, 10 | unit?: string 11 | } = $props(); 12 | </script> 13 | 14 | <button class="border-2 p-2 rounded-xl aspect-square flex flex-col justify-center items-center" 15 | class:w-24={!grid} 16 | class:selected={selected} 17 | onclick={onClick} 18 | > 19 | {#if option.icon != null} 20 | <Icon class="text-4xl" name={option.icon}/> 21 | {/if} 22 | {#if option.icon == null || option.iconAndText} 23 | <span class="whitespace-nowrap overflow-hidden w-full text-xs">{option.text} {unit ?? ""}</span> 24 | {/if} 25 | </button> 26 | 27 | <style> 28 | .selected { 29 | @apply border-2 border-green-500 bg-green-50; 30 | } 31 | </style> 32 | -------------------------------------------------------------------------------- /src/components/form/fields/textArea/TextAreaFormField.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import {type FormFieldProps} from "@perfice/model/form/ui"; 3 | 4 | let {disabled, value, onChange}: FormFieldProps = $props(); 5 | 6 | let input: HTMLTextAreaElement; 7 | 8 | export function focus() { 9 | input.focus(); 10 | } 11 | 12 | function onInputChange(e: { currentTarget: HTMLTextAreaElement }) { 13 | onChange(e.currentTarget.value); 14 | } 15 | </script> 16 | 17 | <textarea spellcheck="false" class="border bg-white md:w-1/2 w-full" {disabled} value={value} onchange={onInputChange} 18 | rows="3" 19 | bind:this={input} 20 | ></textarea> 21 | -------------------------------------------------------------------------------- /src/components/form/valueInput/PrimitiveVanillaInputField.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {PrimitiveValue} from "@perfice/model/primitive/primitive"; 3 | import type {FormQuestionDataType} from "@perfice/model/form/form"; 4 | import {getHtmlInputFromQuestionType} from "@perfice/model/form/ui"; 5 | import {questionDataTypeRegistry} from "@perfice/model/form/data"; 6 | 7 | let {value, dataType, onChange, class: className = ""}: { 8 | value: PrimitiveValue, 9 | dataType: FormQuestionDataType, 10 | onChange: (v: PrimitiveValue) => void, 11 | class?: string 12 | } = $props(); 13 | 14 | let dataDef = $derived(questionDataTypeRegistry.getDefinition(dataType)); 15 | let serialized = $derived(dataDef?.serialize(value) ?? ""); 16 | 17 | function onInputChange(e: { currentTarget: HTMLInputElement }) { 18 | let value = e.currentTarget.value; 19 | if (dataDef == null) 20 | return; 21 | 22 | let deserialized = dataDef.deserialize(value); 23 | if (deserialized == null) 24 | return; 25 | 26 | onChange(deserialized); 27 | } 28 | </script> 29 | 30 | <input class={className} value={serialized} type={getHtmlInputFromQuestionType(dataType)} 31 | onchange={onInputChange}/> 32 | -------------------------------------------------------------------------------- /src/components/goal/GoalMetIndicator.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | // noinspection ES6UnusedImports 3 | import Fa from "svelte-fa"; 4 | 5 | import {faCheck, faTimes} from "@fortawesome/free-solid-svg-icons"; 6 | 7 | let {value}: { value: boolean } = $props(); 8 | </script> 9 | 10 | {#if value} 11 | <Fa icon={faCheck} class="text-green-500"/> 12 | {:else} 13 | <Fa icon={faTimes} class="text-red-500"/> 14 | {/if} 15 | -------------------------------------------------------------------------------- /src/components/goal/GoalNewCard.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import {faPlusCircle} from "@fortawesome/free-solid-svg-icons"; 3 | // noinspection ES6UnusedImports 4 | import Fa from "svelte-fa"; 5 | import {NEW_GOAL_ROUTE} from "@perfice/model/goal/ui"; 6 | import {navigate} from "@perfice/app"; 7 | 8 | function onClick() { 9 | navigate(`/goals/${NEW_GOAL_ROUTE}`); 10 | } 11 | </script> 12 | 13 | <button class="rounded-xl h-48 flex-center border-2 border-dashed text-gray-500 hover-feedback" onclick={onClick}> 14 | <Fa icon={faPlusCircle}/> 15 | </button> 16 | -------------------------------------------------------------------------------- /src/components/goal/editor/condition/comparison/AddSourceButton.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import {faHashtag, faSquareRootVariable} from "@fortawesome/free-solid-svg-icons"; 3 | import PopupContextMenuButton from "@perfice/components/base/contextMenu/PopupContextMenuButton.svelte"; 4 | 5 | let {onAdd}: { onAdd: (constant: boolean) => void } = $props(); 6 | 7 | const GOAL_SOURCE_TYPES = [ 8 | { 9 | name: "Dynamic value", 10 | icon: faSquareRootVariable, 11 | action: () => onAdd(false) 12 | }, 13 | 14 | { 15 | name: "Constant", 16 | icon: faHashtag, 17 | action: () => onAdd(true) 18 | } 19 | ]; 20 | </script> 21 | 22 | <PopupContextMenuButton items={GOAL_SOURCE_TYPES}>Add source</PopupContextMenuButton> 23 | -------------------------------------------------------------------------------- /src/components/goal/editor/sidebar/AddConditionSidebar.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import { 3 | GOAL_CONDITION_TYPES, 4 | type GoalAddConditionAction, 5 | } from "@perfice/model/goal/ui"; 6 | import CardButton from "@perfice/components/base/button/CardButton.svelte"; 7 | import { 8 | createGoalConditionValue, 9 | GoalConditionType, 10 | } from "@perfice/services/variable/types/goal"; 11 | 12 | let { 13 | action, 14 | onClose, 15 | }: { action: GoalAddConditionAction; onClose: () => void } = $props(); 16 | 17 | function onSelect(type: GoalConditionType) { 18 | action.onConditionSelected({ 19 | id: crypto.randomUUID(), 20 | type: type, 21 | // @ts-ignore 22 | value: createGoalConditionValue(type), 23 | }); 24 | 25 | onClose(); 26 | } 27 | </script> 28 | 29 | <div class="flex flex-col gap-4 mt-4"> 30 | {#each GOAL_CONDITION_TYPES as type} 31 | <CardButton 32 | icon={type.icon} 33 | title={type.name} 34 | description={type.description} 35 | onClick={() => onSelect(type.type)} 36 | /> 37 | {/each} 38 | </div> 39 | -------------------------------------------------------------------------------- /src/components/goal/editor/sidebar/AddSourceSidebar.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import { 3 | COMPARISON_SOURCE_TYPES, 4 | type GoalAddSourceAction, 5 | } from "@perfice/model/goal/ui"; 6 | import CardButton from "@perfice/components/base/button/CardButton.svelte"; 7 | import type { VariableTypeName } from "@perfice/model/variable/variable"; 8 | 9 | let { 10 | action, 11 | onClose, 12 | }: { action: GoalAddSourceAction; onClose: () => void } = $props(); 13 | 14 | function onSelect(a: VariableTypeName) { 15 | action.onSourceSelected(a); 16 | onClose(); 17 | } 18 | </script> 19 | 20 | <div class="flex flex-col gap-4 mt-4"> 21 | {#each COMPARISON_SOURCE_TYPES as type} 22 | <CardButton 23 | icon={type.icon} 24 | title={type.name} 25 | description={type.description} 26 | onClick={() => onSelect(type.type)} 27 | /> 28 | {/each} 29 | </div> 30 | -------------------------------------------------------------------------------- /src/components/goal/multi/ConditionEntry.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {GoalConditionValueResult} from "@perfice/stores/goal/value"; 3 | import {GoalConditionType} from "@perfice/services/variable/types/goal"; 4 | import ComparisonConditionEntry from "@perfice/components/goal/multi/ComparisonConditionEntry.svelte"; 5 | import type {Component} from "svelte"; 6 | import GoalMetConditionEntry from "@perfice/components/goal/multi/GoalMetConditionEntry.svelte"; 7 | 8 | let {value, color}: { value: GoalConditionValueResult, color: string } = $props(); 9 | 10 | const RENDERERS: Record<GoalConditionType, Component<{ value: any, color: string }>> = { 11 | [GoalConditionType.COMPARISON]: ComparisonConditionEntry, 12 | [GoalConditionType.GOAL_MET]: GoalMetConditionEntry, 13 | } 14 | 15 | let RendererComponent = $derived(RENDERERS[value.type]); 16 | </script> 17 | 18 | <div class="p-2 gap-2 w-full border-b"> 19 | <RendererComponent value={value.value} {color} /> 20 | </div> 21 | -------------------------------------------------------------------------------- /src/components/goal/multi/GoalMetConditionEntry.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {GoalMetValueResult} from "@perfice/stores/goal/value"; 3 | import GoalMetIndicator from "@perfice/components/goal/GoalMetIndicator.svelte"; 4 | 5 | let {value}: { value: GoalMetValueResult, color: string } = $props(); 6 | </script> 7 | 8 | <div class="row-between"> 9 | <GoalMetIndicator value={value.met}/> 10 | {value.name} 11 | </div> 12 | -------------------------------------------------------------------------------- /src/components/goal/multi/MultiConditionRenderer.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import ConditionEntry from "@perfice/components/goal/multi/ConditionEntry.svelte"; 3 | import type {GoalConditionValueResult} from "@perfice/stores/goal/value"; 4 | 5 | let {value, color}: { value: GoalConditionValueResult[], color: string } = $props(); 6 | </script> 7 | 8 | <div class="flex flex-col items-start w-full h-full overflow-y-scroll scrollbar-hide"> 9 | {#each value as val} 10 | <ConditionEntry value={val} color={color}/> 11 | {/each} 12 | </div> 13 | -------------------------------------------------------------------------------- /src/components/goal/single/ComparisonSingleCondition.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import {type ComparisonValueResult, formatComparisonNumberValues} from "@perfice/stores/goal/value"; 3 | import CircularProgressBar from "@perfice/components/base/progress/CircularProgressBar.svelte"; 4 | import {getGoalConditionProgress} from "@perfice/model/goal/ui"; 5 | 6 | let {value, color}: { value: ComparisonValueResult, color: string } = $props(); 7 | 8 | let {first, second, progress, dataType, unit} = $derived(getGoalConditionProgress(value)); 9 | </script> 10 | 11 | <CircularProgressBar progress={progress} strokeColor={color}> 12 | {formatComparisonNumberValues(first, second, dataType, unit)} 13 | </CircularProgressBar> 14 | -------------------------------------------------------------------------------- /src/components/goal/single/GoalMetSingleCondition.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import GoalMetIndicator from "@perfice/components/goal/GoalMetIndicator.svelte"; 3 | import type {GoalMetValueResult} from "@perfice/stores/goal/value"; 4 | 5 | let {value}: { value: GoalMetValueResult, color: string } = $props(); 6 | </script> 7 | 8 | <div class="flex flex-col text-3xl"> 9 | <GoalMetIndicator value={value.met}/> 10 | <p class="text-base">{value.name}</p> 11 | </div> 12 | -------------------------------------------------------------------------------- /src/components/goal/single/SingleConditionRenderer.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import {GoalConditionType} from "@perfice/services/variable/types/goal"; 3 | import type {Component} from "svelte"; 4 | import ComparisonSingleCondition from "@perfice/components/goal/single/ComparisonSingleCondition.svelte"; 5 | import GoalMetSingleCondition from "@perfice/components/goal/single/GoalMetSingleCondition.svelte"; 6 | import type {GoalConditionValueResult} from "@perfice/stores/goal/value"; 7 | 8 | let {value, color}: { value: GoalConditionValueResult, color: string } = $props(); 9 | 10 | const RENDERERS: Record<GoalConditionType, Component<{ value: any, color: string }>> = { 11 | [GoalConditionType.COMPARISON]: ComparisonSingleCondition, 12 | [GoalConditionType.GOAL_MET]: GoalMetSingleCondition, 13 | } 14 | 15 | let RendererComponent = $derived(RENDERERS[value.type]); 16 | </script> 17 | 18 | <div class="flex-center h-full"> 19 | <RendererComponent value={value.value} {color} /> 20 | </div> 21 | -------------------------------------------------------------------------------- /src/components/journal/day/JournalCardBase.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {Snippet} from "svelte"; 3 | 4 | let {onClick, children, selected = false}: { 5 | onClick: () => void, 6 | children: Snippet, 7 | selected?: boolean 8 | } = $props(); 9 | </script> 10 | 11 | <button 12 | onclick={onClick} 13 | class="bg-gray-100 w-full rounded-xl border px-4 py-1 group" 14 | class:hover-feedback={!selected} 15 | class:bg-gray-300={selected} 16 | > 17 | {@render children()} 18 | </button> 19 | -------------------------------------------------------------------------------- /src/components/journal/day/JournalCardHeader.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import {faTrash} from "@fortawesome/free-solid-svg-icons"; 3 | // noinspection ES6UnusedImports 4 | import Fa from "svelte-fa"; 5 | import JournalEntryTimestamp from "@perfice/components/journal/day/JournalEntryTimestamp.svelte"; 6 | import type {Snippet} from "svelte"; 7 | import type {JournalEntry} from "@perfice/model/journal/journal"; 8 | 9 | let {entry, children, onDelete}: { entry: JournalEntry, children: Snippet, onDelete: () => void } = $props(); 10 | 11 | function onDeleteClick(e: MouseEvent) { 12 | e.stopPropagation(); 13 | onDelete(); 14 | } 15 | </script> 16 | 17 | <div class="w-full flex justify-between items-center overflow-hidden text-ellipsis"> 18 | {@render children()} 19 | 20 | <div class="flex items-center gap-2"> 21 | <button 22 | onclick={onDeleteClick} 23 | class="hidden md:group-hover:block hover:text-red-800" 24 | > 25 | <Fa icon={faTrash}/> 26 | </button> 27 | <div class="group-hover:hidden"> 28 | <JournalEntryTimestamp timestamp={entry.timestamp} /> 29 | </div> 30 | </div> 31 | </div> 32 | -------------------------------------------------------------------------------- /src/components/journal/day/JournalDayDate.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import {MONTHS_SHORT} from "@perfice/util/time/format"; 3 | 4 | let {date, dayOfMonth, weekDay}: { date: Date, dayOfMonth: string, weekDay: string } = $props(); 5 | </script> 6 | 7 | <div 8 | class="border p-4 bg-gray-50 md:inline-block text-center md:w-24 rounded-xl hidden" 9 | > 10 | <p class="text text-[11px] font-bold"> 11 | {MONTHS_SHORT[date.getMonth()]} 12 | </p> 13 | <b class="text-xl">{dayOfMonth}</b> 14 | <p class="text-gray-400 text-[10px]">{weekDay.toUpperCase()}</p> 15 | </div> 16 | -------------------------------------------------------------------------------- /src/components/journal/day/JournalEntryTimestamp.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import {formatTimestampHHMM} from "@perfice/util/time/format.js"; 3 | 4 | let {timestamp}: { timestamp: number } = $props(); 5 | </script> 6 | 7 | <p class="font-bold text-gray-600" style="font-size: 12px;"> 8 | {formatTimestampHHMM(timestamp)} 9 | </p> 10 | -------------------------------------------------------------------------------- /src/components/journal/day/JournalSummaryContainer.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {Snippet} from "svelte"; 3 | 4 | let {children}: { children: Snippet } = $props(); 5 | </script> 6 | 7 | <div class="grid grid-cols-2"> 8 | {@render children()} 9 | </div> 10 | 11 | <style> 12 | .side { 13 | width: 100%; 14 | } 15 | 16 | @media (min-width: 1200px) { 17 | .side { 18 | width: 48%; 19 | } 20 | } 21 | </style> 22 | -------------------------------------------------------------------------------- /src/components/journal/day/JournalTagEntries.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import TagButtonBase from "@perfice/components/tag/TagButtonBase.svelte"; 3 | import type {TransformedTagEntry} from "@perfice/stores/journal/grouped"; 4 | // noinspection ES6UnusedImports 5 | import Fa from "svelte-fa"; 6 | import {faTimes} from "@fortawesome/free-solid-svg-icons"; 7 | import type {JournalEntity, TagEntry} from "@perfice/model/journal/journal"; 8 | 9 | let {tagEntries, onClick, selectedEntities}: { 10 | tagEntries: TransformedTagEntry[], 11 | onClick: (entry: TagEntry) => void, 12 | selectedEntities: JournalEntity[] 13 | } = $props(); 14 | </script> 15 | 16 | {#if tagEntries.length > 0} 17 | <div class="flex gap-2 flex-wrap"> 18 | {#each tagEntries as entry (entry.id)} 19 | <TagButtonBase checked={selectedEntities.some(e => e.entry.id === entry.id)} 20 | onClick={() => onClick(entry)}> 21 | {entry.tag.name} 22 | <Fa icon={faTimes}/> 23 | </TagButtonBase> 24 | {/each} 25 | </div> 26 | {/if} 27 | -------------------------------------------------------------------------------- /src/components/journal/search/common/ByCategoryFilterCard.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import GenericFilterContainer from "@perfice/components/journal/search/types/GenericFilterContainer.svelte"; 3 | import MultiSelectDropdownButton from "@perfice/components/base/dropdown/MultiSelectDropdownButton.svelte"; 4 | import type {ByCategoryFilter} from "@perfice/model/journal/search/search"; 5 | 6 | let {filter, onChange, onDelete, items}: { 7 | filter: ByCategoryFilter, 8 | onChange: (filter: ByCategoryFilter) => void, 9 | onDelete: () => void, 10 | items: { name: string, value: string | null }[] 11 | } = $props(); 12 | </script> 13 | 14 | <GenericFilterContainer name="By category" {onDelete}> 15 | <MultiSelectDropdownButton onChange={(v) => onChange({...filter, categories: v})} noneText="None" 16 | value={filter.categories} 17 | items={items}/> 18 | </GenericFilterContainer> 19 | -------------------------------------------------------------------------------- /src/components/journal/search/common/OneOfFilterCard.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {OneOfFilter} from "@perfice/model/journal/search/search"; 3 | import GenericFilterContainer from "@perfice/components/journal/search/types/GenericFilterContainer.svelte"; 4 | import MultiSelectDropdownButton from "@perfice/components/base/dropdown/MultiSelectDropdownButton.svelte"; 5 | 6 | let {filter, onChange, onDelete, items}: { 7 | filter: OneOfFilter, 8 | onChange: (filter: OneOfFilter) => void, 9 | onDelete: () => void, 10 | items: { name: string, value: string }[] 11 | } = $props(); 12 | </script> 13 | 14 | <GenericFilterContainer name="One of" {onDelete}> 15 | <MultiSelectDropdownButton onChange={(v) => onChange({...filter, values: v})} noneText="None" value={filter.values} 16 | items={items}/> 17 | </GenericFilterContainer> 18 | -------------------------------------------------------------------------------- /src/components/journal/search/types/GenericFilterContainer.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | 3 | import {faFilter, faTrash} from "@fortawesome/free-solid-svg-icons"; 4 | import IconButton from "@perfice/components/base/button/IconButton.svelte"; 5 | import type {Snippet} from "svelte"; 6 | // noinspection ES6UnusedImports 7 | import Fa from "svelte-fa"; 8 | 9 | let {name, onDelete, children}: { name: string, onDelete: () => void, children: Snippet } = $props(); 10 | 11 | </script> 12 | 13 | <div class="border"> 14 | <div class="row-between px-4 py-2"> 15 | <div class="row-gap flex-wrap"> 16 | <Fa icon={faFilter}></Fa> 17 | {name} 18 | {@render children()} 19 | </div> 20 | <IconButton icon={faTrash} onClick={onDelete}/> 21 | </div> 22 | </div> -------------------------------------------------------------------------------- /src/components/journal/search/types/date/DateSearchOptions.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {DateSearch} from "@perfice/model/journal/search/date"; 3 | import RangedTimeScopePicker from "@perfice/components/base/timeScope/RangedTimeScopePicker.svelte"; 4 | import {timeRangeToRangedTimeScope, TimeRangeType} from "@perfice/model/variable/time/time"; 5 | 6 | let {options, onChange}: { options: DateSearch, onChange: (options: DateSearch) => void } = $props(); 7 | 8 | let ranged = $derived(timeRangeToRangedTimeScope(options.range)); 9 | let converted = $derived(ranged.convertToRange()); 10 | </script> 11 | 12 | <div class="p-4"> 13 | {#if converted.type !== TimeRangeType.ALL && ranged.getStart() === ranged.getEnd()} 14 | <span class="text-red-500"> 15 | Empty date range, to select a single date, set "To" as the next day. 16 | </span> 17 | {/if} 18 | <RangedTimeScopePicker value={ranged} 19 | onChange={v => onChange({...options, range: v.convertToRange()})}/> 20 | </div> -------------------------------------------------------------------------------- /src/components/journal/search/types/freeText/FreeTextSearchOptions.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {FreeTextSearch} from "@perfice/model/journal/search/freeText"; 3 | 4 | let {options, onChange}: { options: FreeTextSearch, onChange: (options: FreeTextSearch) => void } = $props(); 5 | </script> 6 | 7 | <div class="p-4"> 8 | <textarea class="w-full" placeholder="Search" 9 | onchange={(e) => onChange({...options, search: e.currentTarget.value})}></textarea> 10 | </div> -------------------------------------------------------------------------------- /src/components/journal/search/types/tag/TagSearchActions.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import { 3 | createTagSearchFilter, 4 | TAG_SEARCH_FILTER_TYPES, 5 | type TagSearch, 6 | type TagSearchFilter, 7 | TagSearchFilterType 8 | } from "@perfice/model/journal/search/tag"; 9 | import {faFilter} from "@fortawesome/free-solid-svg-icons"; 10 | import PopupIconButton from "@perfice/components/base/button/PopupIconButton.svelte"; 11 | 12 | let {options, onChange}: { options: TagSearch, onChange: (search: TagSearch) => void } = $props(); 13 | 14 | function onAddFilter(v: TagSearchFilterType) { 15 | onChange({ 16 | ...options, 17 | filters: [...options.filters, createTagSearchFilter(v)] 18 | }); 19 | } 20 | 21 | let buttons = TAG_SEARCH_FILTER_TYPES.map((t) => { 22 | return { 23 | name: t.name, 24 | icon: t.icon, 25 | action: () => onAddFilter(t.value), 26 | }; 27 | }); 28 | </script> 29 | 30 | <PopupIconButton icon={faFilter} buttons={buttons}/> 31 | -------------------------------------------------------------------------------- /src/components/journal/search/types/tag/filters/TagByCategoryFilterCard.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type { ByCategoryFilter } from "@perfice/model/journal/search/search"; 3 | import type { JournalSearchUiDependencies } from "@perfice/model/journal/search/ui"; 4 | import ByCategoryFilterCard from "@perfice/components/journal/search/common/ByCategoryFilterCard.svelte"; 5 | import { UNCATEGORIZED_NAME } from "@perfice/util/category"; 6 | 7 | let { 8 | filter, 9 | onChange, 10 | onDelete, 11 | dependencies, 12 | }: { 13 | filter: ByCategoryFilter; 14 | onChange: (filter: ByCategoryFilter) => void; 15 | onDelete: () => void; 16 | dependencies: JournalSearchUiDependencies; 17 | } = $props(); 18 | 19 | let items = [ 20 | { name: UNCATEGORIZED_NAME, value: null }, 21 | ...dependencies.tagCategories.map((t) => ({ 22 | name: t.name, 23 | value: t.id, 24 | })), 25 | ]; 26 | </script> 27 | 28 | <ByCategoryFilterCard {items} {onDelete} {onChange} {filter} /> 29 | -------------------------------------------------------------------------------- /src/components/journal/search/types/tag/filters/TagOneOfFilterCard.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type { OneOfFilter } from "@perfice/model/journal/search/search"; 3 | import type { JournalSearchUiDependencies } from "@perfice/model/journal/search/ui"; 4 | import OneOfFilterCard from "@perfice/components/journal/search/common/OneOfFilterCard.svelte"; 5 | 6 | let { 7 | filter, 8 | onChange, 9 | onDelete, 10 | dependencies, 11 | }: { 12 | filter: OneOfFilter; 13 | onChange: (filter: OneOfFilter) => void; 14 | onDelete: () => void; 15 | dependencies: JournalSearchUiDependencies; 16 | } = $props(); 17 | </script> 18 | 19 | <OneOfFilterCard 20 | items={dependencies.tags.map((t) => ({ name: t.name, value: t.id }))} 21 | {onDelete} 22 | {onChange} 23 | {filter} 24 | /> 25 | -------------------------------------------------------------------------------- /src/components/journal/search/types/trackable/TrackableSearchActions.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import { 3 | createTrackableSearchFilter, 4 | TRACKABLE_SEARCH_FILTER_TYPES, 5 | type TrackableSearch, 6 | type TrackableSearchFilter, 7 | TrackableSearchFilterType 8 | } from "@perfice/model/journal/search/trackable"; 9 | import {faFilter} from "@fortawesome/free-solid-svg-icons"; 10 | import PopupIconButton from "@perfice/components/base/button/PopupIconButton.svelte"; 11 | 12 | let {options, onChange}: { options: TrackableSearch, onChange: (search: TrackableSearch) => void } = $props(); 13 | 14 | function onAddFilter(v: TrackableSearchFilterType) { 15 | onChange({ 16 | ...options, 17 | filters: [...options.filters, createTrackableSearchFilter(v)] 18 | }); 19 | } 20 | 21 | let buttons = TRACKABLE_SEARCH_FILTER_TYPES.map((t) => { 22 | return { 23 | name: t.name, 24 | icon: t.icon, 25 | action: () => onAddFilter(t.value), 26 | }; 27 | }); 28 | </script> 29 | 30 | <PopupIconButton icon={faFilter} buttons={buttons}/> 31 | -------------------------------------------------------------------------------- /src/components/journal/search/types/trackable/filters/TrackableByCategoryFilterCard.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {ByCategoryFilter} from "@perfice/model/journal/search/search"; 3 | import type {JournalSearchUiDependencies} from "@perfice/model/journal/search/ui"; 4 | import ByCategoryFilterCard from "@perfice/components/journal/search/common/ByCategoryFilterCard.svelte"; 5 | import {UNCATEGORIZED_NAME} from "@perfice/util/category"; 6 | 7 | let {filter, onChange, onDelete, dependencies}: { 8 | filter: ByCategoryFilter, 9 | onChange: (filter: ByCategoryFilter) => void, 10 | onDelete: () => void, 11 | dependencies: JournalSearchUiDependencies 12 | } = $props(); 13 | 14 | let items = [{name: UNCATEGORIZED_NAME, value: null}, ...dependencies.trackableCategories.map(t => ({ 15 | name: t.name, 16 | value: t.id 17 | }))]; 18 | </script> 19 | 20 | <ByCategoryFilterCard items={items} 21 | onDelete={onDelete} onChange={onChange} filter={filter}/> 22 | -------------------------------------------------------------------------------- /src/components/journal/search/types/trackable/filters/TrackableOneOfFilterCard.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {OneOfFilter} from "@perfice/model/journal/search/search"; 3 | import type {JournalSearchUiDependencies} from "@perfice/model/journal/search/ui"; 4 | import OneOfFilterCard from "@perfice/components/journal/search/common/OneOfFilterCard.svelte"; 5 | 6 | let {filter, onChange, onDelete, dependencies}: { 7 | filter: OneOfFilter, 8 | onChange: (filter: OneOfFilter) => void, 9 | onDelete: () => void, 10 | dependencies: JournalSearchUiDependencies 11 | } = $props(); 12 | </script> 13 | 14 | <OneOfFilterCard items={dependencies.trackables.map(t => ({name: t.name, value: t.id}))} 15 | onDelete={onDelete} onChange={onChange} filter={filter}/> -------------------------------------------------------------------------------- /src/components/mobile/MobileTopBar.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {Snippet} from "svelte"; 3 | import DrawerOpenButton from "@perfice/components/sidebar/drawer/DrawerOpenButton.svelte"; 4 | 5 | let {title, leading, actions, leftTitleOffset}: { 6 | title: string, 7 | leading?: Snippet, 8 | leftTitleOffset?: string, 9 | actions?: Snippet 10 | } = $props(); 11 | </script> 12 | 13 | <div class="w-screen max-w-screen border-b md:hidden p-2 flex items-center sticky z-10 justify-between min-h-12 bg-white"> 14 | <div class="z-10 row-gap"> 15 | {#if leading != null} 16 | {@render leading()} 17 | {:else} 18 | <DrawerOpenButton/> 19 | {/if} 20 | </div> 21 | <div class="z-10 row-gap"> 22 | {@render actions?.()} 23 | </div> 24 | <div class="flex flex-1 w-screen {leftTitleOffset ?? 'justify-center left-0'} absolute font-bold text-lg"> 25 | {title} 26 | </div> 27 | </div> 28 | -------------------------------------------------------------------------------- /src/components/onboarding/OnboardingImage.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {OnboardingImagePage} from "@perfice/model/onboarding/onboarding"; 3 | import {BASE_URL} from "@perfice/app.js"; 4 | 5 | let {page}: { page: OnboardingImagePage } = $props(); 6 | </script> 7 | 8 | <div class="bg-gray-300 rounded-xl w-full h-[60vh] md:h-[50vh] border-2 shadow-md"> 9 | <img src={`${BASE_URL}/${page.desktopImage}`} alt="Onboarding" class="image hidden md:block"/> 10 | <img src={`${BASE_URL}/${page.mobileImage}`} alt="Onboarding" class="image md:hidden"/> 11 | </div> 12 | <div class="flex-col flex justify-between mt-4"> 13 | <div class="md:text-center"> 14 | <h1 class="text-4xl md:text-7xl font-bold text-gray-600">{page.title}</h1> 15 | <p class="md:text-3xl text-2xl text-gray-500 mt-2">{page.description}</p> 16 | </div> 17 | </div> 18 | 19 | <style> 20 | .image { 21 | @apply rounded-xl object-cover w-full h-full; 22 | } 23 | </style> -------------------------------------------------------------------------------- /src/components/onboarding/OnboardingSelect.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import {type OnboardingSelection, type OnboardingSelectPage} from "@perfice/model/onboarding/onboarding"; 3 | import OnboardingSelectButton from "@perfice/components/onboarding/OnboardingSelectButton.svelte"; 4 | 5 | let {page, selectState, updateSelectState}: { 6 | page: OnboardingSelectPage, 7 | selectState?: OnboardingSelection[] 8 | updateSelectState: (selections: OnboardingSelection[]) => void 9 | } = $props(); 10 | </script> 11 | 12 | <h1 class="text-4xl md:text-7xl font-bold text-gray-600">{page.title}</h1> 13 | <p class="text-xl mt-2">{page.description}</p> 14 | <div class="flex flex-col gap-4 mt-4 max-h-[55vh] md:max-h-[45vh] overflow-y-scroll scrollbar-hide"> 15 | {#each page.categories as category} 16 | <div> 17 | <h2 class="mb-2 font-bold text-xl">{category.name}</h2> 18 | <div class="grid md:grid-cols-4 grid-cols-2 gap-2"> 19 | {#each category.items as item} 20 | <OnboardingSelectButton {updateSelectState} {selectState} isDefault={item.default} 21 | category={category.name} item={item}/> 22 | {/each} 23 | </div> 24 | </div> 25 | {/each} 26 | </div> -------------------------------------------------------------------------------- /src/components/reflection/GlobalReflectionModal.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import {subscribeToEventStore} from "@perfice/util/event"; 3 | import {openReflectionEvents} from "@perfice/model/reflection/ui"; 4 | import ReflectionModal from "@perfice/components/reflection/modal/ReflectionModal.svelte"; 5 | 6 | let reflectionModal: ReflectionModal; 7 | 8 | $effect(() => { 9 | subscribeToEventStore($openReflectionEvents, openReflectionEvents, (e) => reflectionModal.open(e)); 10 | }); 11 | </script> 12 | 13 | <ReflectionModal bind:this={reflectionModal}/> 14 | -------------------------------------------------------------------------------- /src/components/reflection/editor/notifications/NotificationCard.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import {faBell} from "@fortawesome/free-solid-svg-icons"; 3 | import GenericEditDeleteCard from "@perfice/components/base/card/GenericEditDeleteCard.svelte"; 4 | import {NOTIFICATION_WEEKDAYS, type StoredNotification} from "@perfice/model/notification/notification"; 5 | import {formatTimestampHHMM} from "@perfice/util/time/format"; 6 | import {utcHhMmToLocal} from "@perfice/util/time/simple"; 7 | 8 | let {notification, onEdit, onDelete}: { 9 | notification: StoredNotification, 10 | onEdit: () => void, 11 | onDelete: () => void, 12 | } = $props(); 13 | 14 | let weekDayTitle = $derived(NOTIFICATION_WEEKDAYS.find(v => v.value == notification.weekDay)?.name ?? ""); 15 | let [localHour, localMinutes] = $derived(utcHhMmToLocal(notification.hour, notification.minutes)); 16 | let timeTitle = $derived(`${localHour.toString().padStart(2, "0")}:${localMinutes.toString().padStart(2, "0")}`); 17 | let title = $derived(`${weekDayTitle} ${timeTitle}`); 18 | </script> 19 | 20 | <GenericEditDeleteCard icon={faBell} text={title} onEdit={onEdit} 21 | onDelete={onDelete}/> 22 | -------------------------------------------------------------------------------- /src/components/reflection/editor/sidebar/widget/ReflectionEditChecklistWidget.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | 3 | import type {ReflectionChecklistWidgetSettings} from "@perfice/model/reflection/widgets/checklist"; 4 | import EditChecklistWidgetSettings 5 | from "@perfice/components/sharedWidgets/checklist/EditChecklistWidgetSettings.svelte"; 6 | import type {Form} from "@perfice/model/form/form"; 7 | 8 | let {settings, onChange, forms}: { 9 | settings: ReflectionChecklistWidgetSettings, 10 | forms: Form[], 11 | onChange: (settings: ReflectionChecklistWidgetSettings) => void 12 | } = $props(); 13 | </script> 14 | 15 | <EditChecklistWidgetSettings {forms} {settings} {onChange}/> -------------------------------------------------------------------------------- /src/components/reflection/editor/sidebar/widget/ReflectionEditFormWidget.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {Form} from "@perfice/model/form/form"; 3 | import BindableDropdownButton from "@perfice/components/base/dropdown/BindableDropdownButton.svelte"; 4 | import type {ReflectionFormWidgetSettings} from "@perfice/model/reflection/widgets/form"; 5 | 6 | let {settings, onChange, forms}: { 7 | settings: ReflectionFormWidgetSettings, 8 | forms: Form[], 9 | onChange: (settings: ReflectionFormWidgetSettings) => void 10 | } = $props(); 11 | 12 | let availableForms = $derived(forms.map(v => { 13 | return {value: v.id, name: v.name} 14 | })); 15 | 16 | function onFormChange(formId: string) { 17 | onChange({...settings, formId}); 18 | } 19 | </script> 20 | <div class="row-between"> 21 | Form 22 | <BindableDropdownButton value={settings.formId} items={availableForms} 23 | onChange={onFormChange}/> 24 | </div> 25 | -------------------------------------------------------------------------------- /src/components/reflection/editor/sidebar/widget/ReflectionEditTableWidget.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | 3 | import type {ReflectionTableWidgetSettings} from "@perfice/model/reflection/widgets/table"; 4 | import EditTableWidgetSettings from "@perfice/components/sharedWidgets/table/EditTableWidgetSettings.svelte"; 5 | import type {Form} from "@perfice/model/form/form"; 6 | 7 | let {settings, onChange, forms}: { 8 | settings: ReflectionTableWidgetSettings, 9 | onChange: (settings: ReflectionTableWidgetSettings) => void, 10 | forms: Form[], 11 | } = $props(); 12 | </script> 13 | 14 | <EditTableWidgetSettings {settings} {onChange} {forms}/> 15 | -------------------------------------------------------------------------------- /src/components/reflection/editor/sidebar/widget/ReflectionEditTagsWidget.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import {UNCATEGORIZED_TAG_CATEGORY_ID} from "@perfice/model/tag/tag"; 3 | import {UNCATEGORIZED_NAME} from "@perfice/util/category"; 4 | import MultiSelectDropdownButton from "@perfice/components/base/dropdown/MultiSelectDropdownButton.svelte"; 5 | import type {ReflectionTagsWidgetSettings} from "@perfice/model/reflection/widgets/tags"; 6 | import {tagCategories} from "@perfice/stores"; 7 | 8 | let {settings, onChange}: { 9 | settings: ReflectionTagsWidgetSettings, 10 | onChange: (settings: ReflectionTagsWidgetSettings) => void 11 | } = $props(); 12 | 13 | let availableCategories = $derived.by(async () => [ 14 | {value: UNCATEGORIZED_TAG_CATEGORY_ID, name: UNCATEGORIZED_NAME}, 15 | ...(await tagCategories.fetchCategories()).map(v => { 16 | return {value: v.id, name: v.name} 17 | })]); 18 | </script> 19 | 20 | <p class="text-lg font-bold">Categories</p> 21 | <p class="text-sm">Select which categories to show</p> 22 | 23 | {#await availableCategories then val} 24 | <MultiSelectDropdownButton class="w-full mt-2" value={settings.categories} items={val} 25 | onChange={(v) => onChange({...settings, categories: v})}/> 26 | {/await} 27 | -------------------------------------------------------------------------------- /src/components/reflection/modal/ReflectionPageButton.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | 3 | import {faArrowLeft, faArrowRight, faCheck} from "@fortawesome/free-solid-svg-icons"; 4 | import Fa from "svelte-fa"; 5 | 6 | let {end, onClick, left}: { end: boolean, onClick: () => void, left: boolean } = $props(); 7 | let disabled = $derived(end && left); 8 | </script> 9 | 10 | <button {disabled} 11 | onclick={onClick} 12 | class="p-4 rounded-md self-center {disabled ? 'bg-gray-200 text-black': 'bg-green-500 pointer-feedback:bg-green-600 text-white'}"> 13 | <Fa icon={left ? faArrowLeft : (end ? faCheck : faArrowRight)}/> 14 | </button> 15 | -------------------------------------------------------------------------------- /src/components/settings/SettingsDataExport.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import Button from "@perfice/components/base/button/Button.svelte"; 3 | import {downloadTextFile} from "@perfice/util/file"; 4 | import {ExportFileType} from "@perfice/services/export/formEntries/export"; 5 | import {completeExport} from "@perfice/stores"; 6 | 7 | async function onExport() { 8 | await downloadTextFile("complete-export.json", ExportFileType.JSON, 9 | JSON.stringify(await completeExport.export())); 10 | } 11 | </script> 12 | 13 | <h3 class="settings-label">Export data</h3> 14 | <div class="row-gap mt-2"> 15 | <Button onClick={onExport}>Export</Button> 16 | </div> 17 | -------------------------------------------------------------------------------- /src/components/settings/SettingsDataImport.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import FileButton from "@perfice/components/base/fileButton/FileButton.svelte"; 3 | import Button from "@perfice/components/base/button/Button.svelte"; 4 | import SegmentedControl from "@perfice/components/base/segmented/SegmentedControl.svelte"; 5 | import {completeImport} from "@perfice/stores"; 6 | 7 | let file = $state<File | null>(null); 8 | let newFormat = $state(true); 9 | 10 | function onFileChange(files: FileList) { 11 | if (files.length == 0) return; 12 | file = files[0]; 13 | } 14 | 15 | function onImport() { 16 | if (file == null) return; 17 | completeImport.import(file, newFormat); 18 | } 19 | </script> 20 | 21 | <h3 class="settings-label">Import data</h3> 22 | <div class="mt-2"> 23 | <SegmentedControl class="w-64" segments={[ 24 | { 25 | name: "New format", 26 | value: true 27 | }, 28 | { 29 | name: "Old format", 30 | value: false 31 | } 32 | ]} value={newFormat} onChange={(v) => newFormat = v}/> 33 | </div> 34 | 35 | <FileButton class="w-full" displayFile={true} onChange={onFileChange}/> 36 | <Button class="mt-2" onClick={onImport}>Import</Button> 37 | -------------------------------------------------------------------------------- /src/components/settings/SettingsDeleteData.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import Button from "@perfice/components/base/button/Button.svelte"; 3 | import {ButtonColor} from "@perfice/model/ui/button"; 4 | import GenericDeleteModal from "@perfice/components/base/modal/generic/GenericDeleteModal.svelte"; 5 | import {deletion} from "@perfice/stores"; 6 | 7 | let modal: GenericDeleteModal<string>; 8 | 9 | function startDelete() { 10 | modal.open(""); 11 | } 12 | 13 | async function onDeleteData() { 14 | await deletion.deleteAllData(); 15 | window.location.reload(); 16 | } 17 | </script> 18 | 19 | <GenericDeleteModal bind:this={modal} 20 | message="Are you sure you want to delete all your data? This action is irreversible." 21 | onDelete={onDeleteData}/> 22 | 23 | <h3 class="settings-label">Delete data</h3> 24 | <div class="row-gap mt-2"> 25 | <Button class="md:w-auto w-full" color={ButtonColor.RED} onClick={startDelete}>Delete all data</Button> 26 | </div> 27 | -------------------------------------------------------------------------------- /src/components/sharedWidgets/checklist/EditChecklistConditionCard.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import GenericEditDeleteCard from "@perfice/components/base/card/GenericEditDeleteCard.svelte"; 3 | import type {ChecklistCondition} from "@perfice/model/sharedWidgets/checklist/checklist"; 4 | 5 | let {condition, onDelete, onEdit}: { 6 | condition: ChecklistCondition, 7 | onDelete: () => void, 8 | onEdit: () => void 9 | } = $props(); 10 | </script> 11 | 12 | <GenericEditDeleteCard {onEdit} {onDelete} 13 | text={condition.name}/> 14 | 15 | -------------------------------------------------------------------------------- /src/components/sharedWidgets/checklist/EditTagChecklistCondition.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {Tag} from "@perfice/model/tag/tag"; 3 | import {faTags} from "@fortawesome/free-solid-svg-icons"; 4 | import IconLabel from "@perfice/components/base/iconLabel/IconLabel.svelte"; 5 | import DropdownButton from "@perfice/components/base/dropdown/DropdownButton.svelte"; 6 | import type {ChecklistTagCondition} from "@perfice/model/sharedWidgets/checklist/checklist"; 7 | 8 | let {tags, value, onChange}: { 9 | tags: Tag[], 10 | value: ChecklistTagCondition, 11 | onChange: (v: ChecklistTagCondition) => void 12 | } = $props(); 13 | 14 | function onTagChanged(tagId: string) { 15 | onChange({...value, tagId}); 16 | } 17 | 18 | const tagDropdownItems = $derived(tags.map(v => { 19 | return { 20 | value: v.id, 21 | name: v.name, 22 | } 23 | })); 24 | </script> 25 | 26 | <div class="row-between"> 27 | <IconLabel icon={faTags} title="Tag"/> 28 | <DropdownButton value={value.tagId} items={tagDropdownItems} onChange={onTagChanged}/> 29 | </div> 30 | -------------------------------------------------------------------------------- /src/components/sidebar/SidebarButton.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {SidebarLink} from "@perfice/model/ui/sidebar"; 3 | // noinspection ES6UnusedImports 4 | import Fa from "svelte-fa"; 5 | import {navigate} from "@perfice/app"; 6 | 7 | let {link, active}: { link: SidebarLink, active: boolean } = $props(); 8 | 9 | function onClick() { 10 | navigate(link.path); 11 | } 12 | 13 | function getActiveClass(active: boolean) { 14 | if (active) { 15 | return "text-green-600 md:bg-green-700"; 16 | } else { 17 | return "md:bg-green-600 text-gray-600"; 18 | } 19 | } 20 | 21 | let hiddenOnMobile = $derived(link.showOnMobile === false || link.bottom === true); 22 | </script> 23 | <button onclick={onClick} 24 | class:hidden={hiddenOnMobile} 25 | class:flex={!hiddenOnMobile} 26 | class="md:flex flex-col items-center flex-1 md:flex-auto {getActiveClass(active)} md:hover:bg-green-700 md:w-10 md:h-10 27 | md:text-white justify-center rounded-xl text-xl"> 28 | <Fa icon={link.icon}></Fa> 29 | <span class="text-xs md:hidden block">{link.title}</span> 30 | </button> 31 | -------------------------------------------------------------------------------- /src/components/sidebar/drawer/DrawerButton.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | // noinspection ES6UnusedImports 3 | import Fa from "svelte-fa"; 4 | import type {SidebarLink} from "@perfice/model/ui/sidebar"; 5 | import {toggleDrawer} from "@perfice/stores/ui/drawer"; 6 | import {navigate} from "@perfice/app"; 7 | 8 | let {link, active}: { link: SidebarLink, active: boolean } = $props(); 9 | 10 | function onClick() { 11 | navigate(link.path); 12 | toggleDrawer(); 13 | } 14 | </script> 15 | 16 | <button class="flex p-3 row-gap hover-feedback w-full" 17 | class:text-gray-500={!active} class:text-green-500={active} 18 | onclick={onClick}> 19 | <Fa class="w-6" icon={link.icon}/> 20 | <span>{link.title}</span> 21 | </button> 22 | -------------------------------------------------------------------------------- /src/components/sidebar/drawer/DrawerOpenButton.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import IconButton from "@perfice/components/base/button/IconButton.svelte"; 3 | import {faBars} from "@fortawesome/free-solid-svg-icons"; 4 | import {toggleDrawer} from "@perfice/stores/ui/drawer.js"; 5 | </script> 6 | 7 | <IconButton icon={faBars} onClick={() => setTimeout(toggleDrawer)}/> 8 | -------------------------------------------------------------------------------- /src/components/tag/FilteredTagCategories.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import {type CategoryList, UNCATEGORIZED_NAME} from "@perfice/util/category"; 3 | import {type Tag, type TagCategory, UNCATEGORIZED_TAG_CATEGORY_ID} from "@perfice/model/tag/tag"; 4 | import type {Snippet} from "svelte"; 5 | 6 | let {categories, visibleCategories, item}: { 7 | categories: CategoryList<TagCategory, Tag>[], 8 | visibleCategories: string[], 9 | item: Snippet<[Tag]> 10 | } = $props(); 11 | </script> 12 | <div class="flex flex-col gap-2"> 13 | {#each categories as category (category.category?.id)} 14 | {#if visibleCategories.length === 0 || visibleCategories.includes(category.category?.id ?? UNCATEGORIZED_TAG_CATEGORY_ID)} 15 | <div> 16 | <p class="font-bold text-gray-600 text-lg mb-1">{category.category?.name ?? UNCATEGORIZED_NAME}</p> 17 | <div class="flex flex-wrap gap-1"> 18 | {#each category.items as tag} 19 | {@render item(tag)} 20 | {/each} 21 | </div> 22 | </div> 23 | {/if} 24 | {/each} 25 | </div> 26 | -------------------------------------------------------------------------------- /src/components/tag/TagButtonBase.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {Snippet} from "svelte"; 3 | 4 | let {checked, onClick, children}: { checked: boolean, onClick: () => void, children: Snippet } = $props(); 5 | </script> 6 | 7 | <button 8 | onclick={onClick} 9 | class="{checked 10 | ? 'bg-green-500 pointer-feedback:bg-green-600 text-white border-green-600' 11 | : 'bg-white pointer-feedback:bg-gray-200 text-black'} border-2 rounded-xl px-3 py-2 text-xs flex justify-between gap-2 items-center" 12 | > 13 | {@render children()} 14 | </button> 15 | -------------------------------------------------------------------------------- /src/components/tag/TagCard.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import TagButtonBase from "@perfice/components/tag/TagButtonBase.svelte"; 3 | import {faPen, faPlus} from "@fortawesome/free-solid-svg-icons"; 4 | import type {Tag} from "@perfice/model/tag/tag"; 5 | // noinspection ES6UnusedImports 6 | import Fa from "svelte-fa"; 7 | 8 | let {tag, checked, onClick, editing = false}: { 9 | tag: Tag, 10 | checked: boolean, 11 | onClick: () => void, 12 | editing?: boolean 13 | } = $props(); 14 | 15 | function getIcon(checked: boolean, editing: boolean) { 16 | if (editing) { 17 | return faPen; 18 | } 19 | 20 | return checked ? faPlus : faPlus; 21 | } 22 | </script> 23 | <TagButtonBase {checked} {onClick}> 24 | <span class="text-ellipsis overflow-hidden">{tag.name}</span> 25 | <span class="w-2"> 26 | <Fa icon={getIcon(checked, editing)}/> 27 | </span> 28 | </TagButtonBase> 29 | -------------------------------------------------------------------------------- /src/components/tag/TagValueCard.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {Tag} from "@perfice/model/tag/tag"; 3 | // noinspection ES6UnusedImports 4 | import Fa from "svelte-fa"; 5 | import type {WeekStart} from "@perfice/model/variable/time/time"; 6 | import TagCard from "@perfice/components/tag/TagCard.svelte"; 7 | import {tagValue} from "@perfice/stores"; 8 | 9 | let {tag, date, weekStart, onClick, editing = false}: { 10 | tag: Tag, 11 | date: Date, 12 | weekStart: WeekStart, 13 | editing?: boolean, 14 | onClick: (entryId: string | null) => void 15 | } = $props(); 16 | 17 | let tagEntry = $derived(tagValue(tag, date, weekStart, tag.id)); 18 | </script> 19 | 20 | <!-- This should always be resolved since we have a default value in the variable store --> 21 | {#await $tagEntry then entryId} 22 | <TagCard {tag} {editing} checked={entryId != null} onClick={() => onClick(entryId)}/> 23 | {/await} 24 | 25 | -------------------------------------------------------------------------------- /src/components/trackable/card/chart/ChartTrackableRenderer.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import {type PrimitiveValue, PrimitiveValueType} from "@perfice/model/primitive/primitive"; 3 | import type {TrackableChartSettings} from "@perfice/model/trackable/trackable"; 4 | import {getChartColors} from "@perfice/util/color"; 5 | import SingleChart from "@perfice/components/chart/SingleChart.svelte"; 6 | 7 | let {value, cardSettings}: { value: PrimitiveValue, cardSettings: TrackableChartSettings, date: Date } = $props(); 8 | 9 | let dataPoints = $derived.by(() => { 10 | if (value.type == PrimitiveValueType.LIST) { 11 | return value.value 12 | .map(v => v?.value as number ?? 0) 13 | .toReversed(); 14 | } 15 | 16 | return []; 17 | }); 18 | 19 | 20 | let {fillColor, borderColor} = $derived(getChartColors(cardSettings.color)); 21 | </script> 22 | 23 | 24 | <div class="w-full h-full rounded-md"> 25 | <SingleChart type="line" fillColor={fillColor} borderColor={borderColor} hideGrid={true} hideLabels={true} 26 | dataPoints={dataPoints} 27 | labels={dataPoints.map((_, i) => i.toString())}/> 28 | </div> 29 | -------------------------------------------------------------------------------- /src/components/trackable/card/value/table/TableTrackableRenderer.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import {type PrimitiveValue} from "@perfice/model/primitive/primitive"; 3 | import type {TableTrackableValueSettings, TrackableValueSettings} from "@perfice/model/trackable/trackable"; 4 | import TrackableTableEntry from "@perfice/components/trackable/card/value/table/TrackableTableEntry.svelte"; 5 | 6 | let {values, cardSettings}: { 7 | values: PrimitiveValue[], 8 | cardSettings: TrackableValueSettings, 9 | valueSettings: TableTrackableValueSettings, 10 | date: Date 11 | } = $props(); 12 | </script> 13 | <div class="flex h-full flex-col overflow-y-scroll scrollbar-hide"> 14 | {#each values as value} 15 | <TrackableTableEntry value={value} representation={cardSettings.representation}/> 16 | {:else} 17 | <div class="justify-center items-center flex flex-1"> 18 | No values 19 | </div> 20 | {/each} 21 | </div> 22 | -------------------------------------------------------------------------------- /src/components/trackable/card/value/table/TrackableTableEntry.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import { 3 | type DisplayValue, type JournalEntryValue, 4 | pJournalEntry, 5 | type PrimitiveValue, 6 | PrimitiveValueType 7 | } from "@perfice/model/primitive/primitive"; 8 | import type {TextOrDynamic} from "@perfice/model/variable/variable"; 9 | import {formatTimestampHHMM} from "@perfice/util/time/format"; 10 | import {formatAnswersIntoRepresentation} from "@perfice/model/trackable/ui"; 11 | 12 | let {value, representation}: { 13 | value: PrimitiveValue, 14 | representation: TextOrDynamic[]; 15 | } = $props(); 16 | 17 | let entry = $derived<JournalEntryValue>(value.type == PrimitiveValueType.JOURNAL_ENTRY ? value.value : { 18 | id: "", 19 | timestamp: 0, 20 | value: {} 21 | }); 22 | </script> 23 | 24 | <div class="w-full border-b row-between px-2"> 25 | <span> 26 | {formatAnswersIntoRepresentation(entry.value, representation)} 27 | </span> 28 | <span class="text-xs"> 29 | {formatTimestampHHMM(entry.timestamp)} 30 | </span> 31 | </div> 32 | -------------------------------------------------------------------------------- /src/components/trackable/modals/edit/general/EditTrackableCategory.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import {faBoxesStacked} from "@fortawesome/free-solid-svg-icons"; 3 | import DropdownButton from "@perfice/components/base/dropdown/DropdownButton.svelte"; 4 | import IconLabelBetween from "@perfice/components/base/iconLabel/IconLabelBetween.svelte"; 5 | import type {TrackableCategory} from "@perfice/model/trackable/trackable"; 6 | import {UNCATEGORIZED_NAME} from "@perfice/util/category"; 7 | 8 | let {categories, categoryId, onChange}: { 9 | categories: TrackableCategory[], 10 | categoryId: string | null, 11 | onChange: (id: string | null) => void 12 | } = $props(); 13 | 14 | let dropdownItems = $derived([{id: null, name: UNCATEGORIZED_NAME}, ...categories].map(c => { 15 | return { 16 | name: c.name, 17 | value: c.id, 18 | action: () => onChange(c.id) 19 | } 20 | })); 21 | </script> 22 | 23 | <IconLabelBetween title="Category" icon={faBoxesStacked}> 24 | <DropdownButton value={categoryId} items={dropdownItems}/> 25 | </IconLabelBetween> 26 | -------------------------------------------------------------------------------- /src/components/trackable/modals/edit/general/tally/EditTrackableTallyCard.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {FormQuestion} from "@perfice/model/form/form"; 3 | import type {TrackableTallySettings} from "@perfice/model/trackable/trackable"; 4 | 5 | let {}: { 6 | cardSettings: TrackableTallySettings, 7 | availableQuestions: FormQuestion[] 8 | onChange: (settings: any) => void 9 | } = $props(); 10 | </script> 11 | -------------------------------------------------------------------------------- /src/components/trackable/modals/edit/general/value/EditTrackableValueRepresentation.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {TextOrDynamic} from "@perfice/model/variable/variable"; 3 | import type {FormQuestion} from "@perfice/model/form/form"; 4 | import EditTextOrDynamic from "@perfice/components/base/textOrDynamic/EditTextOrDynamic.svelte"; 5 | 6 | let {representation, availableQuestions, onChange}: { 7 | representation: TextOrDynamic[], 8 | availableQuestions: FormQuestion[], 9 | onChange: (v: TextOrDynamic[]) => void 10 | } = $props(); 11 | </script> 12 | 13 | <EditTextOrDynamic value={representation} availableDynamic={availableQuestions} 14 | {onChange} 15 | getDynamicId={(v) => v.id} 16 | getDynamicText={(v) => v.name}/> 17 | 18 | -------------------------------------------------------------------------------- /src/components/variable/edit/EditBackButton.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import {ButtonColor} from "@perfice/model/ui/button.js"; 3 | import {faCheck} from "@fortawesome/free-solid-svg-icons"; 4 | import Button from "@perfice/components/base/button/Button.svelte"; 5 | // noinspection ES6UnusedImports 6 | import Fa from "svelte-fa"; 7 | 8 | let {onBack}: { onBack: () => void } = $props(); 9 | </script> 10 | 11 | <Button class="flex justify-center items-center" color={ButtonColor.WHITE} onClick={onBack}> 12 | <Fa icon={faCheck}/> 13 | </Button> 14 | -------------------------------------------------------------------------------- /src/components/variable/edit/EditVariableName.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {Variable} from "@perfice/model/variable/variable"; 3 | 4 | import {variableEditProvider} from "@perfice/stores"; 5 | 6 | let {variable, onChange}: { variable: Variable, onChange: (v: Variable) => void } = $props(); 7 | 8 | function onNameChange(e: { currentTarget: HTMLInputElement }) { 9 | // @ts-ignore 10 | let variableSnapshot: Variable | null = $state.snapshot<Variable | null>(variable); 11 | if (variableSnapshot == null) return; 12 | 13 | let update: Variable = { 14 | ...variableSnapshot, 15 | // @ts-ignore 16 | type: {...variableSnapshot.type, value: variable!.type.value}, 17 | name: e.currentTarget.value 18 | } 19 | // TODO: can't use $state.snapshot on class instances 20 | variableEditProvider.updateVariable(update); 21 | onChange(update); 22 | } 23 | </script> 24 | 25 | <div class="flex justify-end"> 26 | <input value={variable.name} type="text" class="border" onchange={onNameChange}/> 27 | </div> 28 | -------------------------------------------------------------------------------- /src/db/dexie/analytics.ts: -------------------------------------------------------------------------------- 1 | import type {AnalyticsSettingsCollection} from "@perfice/db/collections"; 2 | import type {EntityTable} from "dexie"; 3 | import type {AnalyticsSettings} from "@perfice/model/analytics/analytics"; 4 | 5 | export class DexieAnalyticsSettingsCollection implements AnalyticsSettingsCollection { 6 | 7 | private table: EntityTable<AnalyticsSettings, "formId">; 8 | 9 | constructor(table: EntityTable<AnalyticsSettings, "formId">) { 10 | this.table = table; 11 | } 12 | 13 | async insertSettings(settings: AnalyticsSettings): Promise<void> { 14 | await this.table.add(settings); 15 | } 16 | 17 | async updateSettings(settings: AnalyticsSettings): Promise<void> { 18 | await this.table.put(settings); 19 | } 20 | 21 | async getAllSettings(): Promise<AnalyticsSettings[]> { 22 | return this.table.toArray(); 23 | } 24 | 25 | async getSettingsByFormId(formId: string): Promise<AnalyticsSettings | undefined> { 26 | return this.table.get(formId); 27 | } 28 | 29 | async deleteSettingsByFormId(formId: string): Promise<void> { 30 | await this.table.delete(formId); 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /src/db/dexie/goal.ts: -------------------------------------------------------------------------------- 1 | import type {EntityTable} from "dexie"; 2 | import type {GoalCollection} from "@perfice/db/collections"; 3 | import type {Goal} from "@perfice/model/goal/goal"; 4 | 5 | export class DexieGoalCollection implements GoalCollection { 6 | 7 | private table: EntityTable<Goal, "id">; 8 | 9 | constructor(table: EntityTable<Goal, "id">) { 10 | this.table = table; 11 | } 12 | 13 | getGoalByVariableId(goalVariableId: string): Promise<Goal | undefined> { 14 | return this.table.where("variableId").equals(goalVariableId).first(); 15 | } 16 | 17 | async getGoalById(id: string): Promise<Goal | undefined> { 18 | return this.table.get(id); 19 | } 20 | 21 | async getGoals(): Promise<Goal[]> { 22 | return this.table.toArray(); 23 | } 24 | 25 | async createGoal(goal: Goal): Promise<void> { 26 | await this.table.add(goal); 27 | } 28 | 29 | async updateGoal(goal: Goal): Promise<void> { 30 | await this.table.put(goal); 31 | } 32 | 33 | async deleteGoalById(id: string): Promise<void> { 34 | await this.table.delete(id); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/db/dexie/migration.ts: -------------------------------------------------------------------------------- 1 | import type {DexieDB} from "@perfice/db/dexie/db"; 2 | import type {Table} from "dexie"; 3 | import type {Migration, Migrator} from "@perfice/db/migration/migration"; 4 | 5 | export class DexieMigrator implements Migrator { 6 | private readonly db: DexieDB; 7 | 8 | constructor(db: DexieDB) { 9 | this.db = db; 10 | } 11 | 12 | async applyMigration(migration: Migration): Promise<void> { 13 | // @ts-ignore 14 | let collection: Table<object> | undefined = this.db[migration.getEntityType()]; 15 | if (collection == undefined) { 16 | console.error("Missing collection for entity type", migration.getEntityType()); 17 | return; 18 | } 19 | 20 | let allEntities = await collection.toArray(); 21 | let result: object[] = []; 22 | for (let entity of allEntities) { 23 | result.push(await migration.apply(structuredClone(entity))); 24 | } 25 | 26 | await collection.bulkPut(result); 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/db/dexie/reflection.ts: -------------------------------------------------------------------------------- 1 | import type {Reflection} from "@perfice/model/reflection/reflection"; 2 | import type {EntityTable} from "dexie"; 3 | import type {ReflectionCollection} from "@perfice/db/collections"; 4 | 5 | export class DexieReflectionCollection implements ReflectionCollection { 6 | 7 | private table: EntityTable<Reflection, "id">; 8 | 9 | constructor(table: EntityTable<Reflection, "id">) { 10 | this.table = table; 11 | } 12 | 13 | async getReflections(): Promise<Reflection[]> { 14 | return this.table.toArray(); 15 | } 16 | 17 | async getReflectionById(id: string): Promise<Reflection | undefined> { 18 | return this.table.get(id); 19 | } 20 | 21 | async createReflection(reflection: Reflection): Promise<void> { 22 | await this.table.add(reflection); 23 | } 24 | 25 | async updateReflection(reflection: Reflection): Promise<void> { 26 | await this.table.put(reflection); 27 | } 28 | 29 | async deleteReflectionById(id: string): Promise<void> { 30 | await this.table.delete(id); 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /src/db/dexie/search.ts: -------------------------------------------------------------------------------- 1 | import type {SavedSearchCollection} from "../collections"; 2 | import type {JournalSearch} from "@perfice/model/journal/search/search"; 3 | import type {EntityTable} from "dexie"; 4 | 5 | export class DexieSavedSearchCollection implements SavedSearchCollection { 6 | 7 | private readonly table: EntityTable<JournalSearch, "id">; 8 | 9 | constructor(table: EntityTable<JournalSearch, "id">) { 10 | this.table = table; 11 | } 12 | 13 | async getSavedSearches(): Promise<JournalSearch[]> { 14 | return this.table.toArray(); 15 | } 16 | 17 | async getSavedSearchById(id: string): Promise<JournalSearch | undefined> { 18 | return this.table.get(id); 19 | } 20 | 21 | async putSavedSearch(search: JournalSearch): Promise<void> { 22 | await this.table.put(search); 23 | } 24 | 25 | async deleteSavedSearchById(id: string): Promise<void> { 26 | await this.table.delete(id); 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/db/dexie/variable.ts: -------------------------------------------------------------------------------- 1 | import type {VariableCollection} from "@perfice/db/collections"; 2 | import {type EntityTable} from "dexie"; 3 | import type {StoredVariable} from "@perfice/model/variable/variable"; 4 | 5 | export class DexieVariableCollection implements VariableCollection { 6 | 7 | private table: EntityTable<StoredVariable, "id">; 8 | 9 | constructor(table: EntityTable<StoredVariable, "id">) { 10 | this.table = table; 11 | } 12 | 13 | getVariableById(id: string): Promise<StoredVariable | undefined> { 14 | return this.table.get(id); 15 | } 16 | 17 | async getVariables(): Promise<StoredVariable[]> { 18 | return this.table.toArray(); 19 | } 20 | 21 | async createVariable(variable: StoredVariable): Promise<void> { 22 | await this.table.add(variable); 23 | } 24 | 25 | async updateVariable(variable: StoredVariable): Promise<void> { 26 | await this.table.put(variable); 27 | } 28 | 29 | async deleteVariableById(id: string): Promise<void> { 30 | await this.table.delete(id); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/db/migration/migrations/chartTitles.ts: -------------------------------------------------------------------------------- 1 | import type {Migration} from "@perfice/db/migration/migration"; 2 | 3 | export class ChartTitlesMigration implements Migration { 4 | async apply(entity: any): Promise<object> { 5 | if (entity.type != "CHART") { 6 | return entity; 7 | } 8 | 9 | return { 10 | ...entity, 11 | settings: { 12 | ...entity.settings, 13 | title: null 14 | } 15 | }; 16 | } 17 | 18 | getEntityType(): string { 19 | return "dashboardWidgets"; 20 | } 21 | 22 | getVersion(): number { 23 | return 1; 24 | } 25 | } -------------------------------------------------------------------------------- /src/db/migration/migrations/defaultQuestionValues.ts: -------------------------------------------------------------------------------- 1 | import type {Migration} from "@perfice/db/migration/migration"; 2 | import type {Form} from "@perfice/model/form/form"; 3 | 4 | export class FormQuestionDefaultValuesMigration implements Migration { 5 | async apply(entity: Form): Promise<object> { 6 | return { 7 | ...entity, questions: entity.questions.map(q => { 8 | return { 9 | ...q, 10 | defaultValue: null 11 | } 12 | }) 13 | }; 14 | } 15 | 16 | getEntityType(): string { 17 | return "forms"; 18 | } 19 | 20 | getVersion(): number { 21 | return 2; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import './app.css' 2 | import {mount} from "svelte"; 3 | import App from "@perfice/App.svelte"; 4 | import 'es-iterator-helpers/auto'; 5 | 6 | const app = mount(App, { 7 | target: document.getElementById('app')!, 8 | }); 9 | 10 | export default app -------------------------------------------------------------------------------- /src/model/analytics/analytics.ts: -------------------------------------------------------------------------------- 1 | export interface AnalyticsSettings { 2 | formId: string; 3 | questionId: string; 4 | useMeanValue: Record<string, boolean>; // Question id -> boolean 5 | // Whether to create values between all entries with the last value 6 | interpolate: boolean; 7 | } -------------------------------------------------------------------------------- /src/model/analytics/ui.ts: -------------------------------------------------------------------------------- 1 | import type { SegmentedItem } from "@perfice/model/ui/segmented"; 2 | 3 | export enum AnalyticsViewType { 4 | TRACKABLES, 5 | TAGS, 6 | CORRELATIONS 7 | } 8 | 9 | export const ANALYTICS_SEGMENTED_ITEMS: SegmentedItem<AnalyticsViewType>[] = [ 10 | { name: "Trackables", value: AnalyticsViewType.TRACKABLES }, 11 | { name: "Tags", value: AnalyticsViewType.TAGS }, 12 | { name: "Correlations", value: AnalyticsViewType.CORRELATIONS }, 13 | ]; 14 | 15 | export function getAnalyticsDetailsLink(type: string, id: string): string { 16 | return `/analytics/${type}:${id}`; 17 | } 18 | -------------------------------------------------------------------------------- /src/model/form/display/input.ts: -------------------------------------------------------------------------------- 1 | import {faKeyboard, type IconDefinition} from "@fortawesome/free-solid-svg-icons"; 2 | import type {FormDisplayTypeDefinition} from "@perfice/model/form/display"; 3 | import {type PrimitiveValue} from "@perfice/model/primitive/primitive"; 4 | 5 | export interface InputFormQuestionSettings { 6 | } 7 | 8 | export class InputFieldDefinition implements FormDisplayTypeDefinition<InputFormQuestionSettings> { 9 | getDisplayValue(value: PrimitiveValue, displaySettings: InputFormQuestionSettings): PrimitiveValue | null { 10 | return null; 11 | } 12 | 13 | hasMultiple(_s: InputFormQuestionSettings): boolean { 14 | return false; 15 | } 16 | 17 | validate(): string | null { 18 | return null; 19 | } 20 | 21 | getDefaultSettings(): InputFormQuestionSettings { 22 | return {}; 23 | } 24 | 25 | onDataTypeChanged(s: InputFormQuestionSettings, dataType: string): InputFormQuestionSettings { 26 | return s; 27 | } 28 | 29 | getName(): string { 30 | return "Input"; 31 | } 32 | 33 | getIcon(): IconDefinition { 34 | return faKeyboard; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/model/form/display/rich-input.ts: -------------------------------------------------------------------------------- 1 | import type {FormDisplayTypeDefinition} from "@perfice/model/form/display"; 2 | import type {PrimitiveValue} from "@perfice/model/primitive/primitive"; 3 | import {faFont, faRulerHorizontal, type IconDefinition} from "@fortawesome/free-solid-svg-icons"; 4 | 5 | export interface RichInputFormQuestionSettings { 6 | } 7 | 8 | export class RichInputFieldDefinition implements FormDisplayTypeDefinition<RichInputFormQuestionSettings> { 9 | validate(value: PrimitiveValue): string | null { 10 | return null; 11 | } 12 | 13 | hasMultiple(s: RichInputFormQuestionSettings): boolean { 14 | return false; 15 | } 16 | 17 | getDefaultSettings(): RichInputFormQuestionSettings { 18 | return {}; 19 | } 20 | 21 | getDisplayValue(value: PrimitiveValue, displaySettings: RichInputFormQuestionSettings): PrimitiveValue | null { 22 | return null; 23 | } 24 | 25 | onDataTypeChanged(s: RichInputFormQuestionSettings, dataType: string): RichInputFormQuestionSettings { 26 | return s; 27 | } 28 | 29 | getName(): string { 30 | return "Rich input"; 31 | } 32 | 33 | getIcon(): IconDefinition { 34 | return faFont; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/model/form/display/text-area.ts: -------------------------------------------------------------------------------- 1 | import {faKeyboard, faParagraph, type IconDefinition} from "@fortawesome/free-solid-svg-icons"; 2 | import type {FormDisplayTypeDefinition} from "@perfice/model/form/display"; 3 | import {type PrimitiveValue} from "@perfice/model/primitive/primitive"; 4 | 5 | export interface TextAreaFormQuestionSettings { 6 | } 7 | 8 | export class TextAreaFieldDefinition implements FormDisplayTypeDefinition<TextAreaFormQuestionSettings> { 9 | getDisplayValue(value: PrimitiveValue, displaySettings: TextAreaFormQuestionSettings): PrimitiveValue | null { 10 | return null; 11 | } 12 | 13 | hasMultiple(_s: TextAreaFormQuestionSettings): boolean { 14 | return false; 15 | } 16 | 17 | validate(): string | null { 18 | return null; 19 | } 20 | 21 | getDefaultSettings(): TextAreaFormQuestionSettings { 22 | return {}; 23 | } 24 | 25 | onDataTypeChanged(s: TextAreaFormQuestionSettings, dataType: string): TextAreaFormQuestionSettings { 26 | return s; 27 | } 28 | 29 | getName(): string { 30 | return "Text area"; 31 | } 32 | 33 | getIcon(): IconDefinition { 34 | return faParagraph; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/model/goal/goal.ts: -------------------------------------------------------------------------------- 1 | export interface Goal { 2 | id: string; 3 | name: string; 4 | color: string; 5 | variableId: string; 6 | streakVariableId: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/model/journal/journal.ts: -------------------------------------------------------------------------------- 1 | import type {PrimitiveValue} from "@perfice/model/primitive/primitive"; 2 | 3 | export interface JournalEntry { 4 | id: string; 5 | timestamp: number; 6 | 7 | formId: string; 8 | snapshotId: string; 9 | 10 | displayValue: string; 11 | answers: Record<string, PrimitiveValue>; 12 | } 13 | 14 | export interface TagEntry { 15 | id: string; 16 | timestamp: number; 17 | tagId: string; 18 | } 19 | 20 | export enum JournalEntityType { 21 | FORM_ENTRY, 22 | TAG_ENTRY 23 | } 24 | 25 | export type JournalEntity = { 26 | type: JournalEntityType.FORM_ENTRY, 27 | entry: JournalEntry 28 | } | { 29 | type: JournalEntityType.TAG_ENTRY, 30 | entry: TagEntry 31 | } 32 | 33 | export function jeForm(entry: JournalEntry): JournalEntity { 34 | return { 35 | type: JournalEntityType.FORM_ENTRY, 36 | entry 37 | } 38 | } 39 | 40 | export function jeTag(entry: TagEntry): JournalEntity { 41 | return { 42 | type: JournalEntityType.TAG_ENTRY, 43 | entry 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/model/journal/search/date.ts: -------------------------------------------------------------------------------- 1 | import {type SearchDefinition, type SearchDependencies, SearchEntityMode} from "@perfice/model/journal/search/search"; 2 | import type {JournalEntry, TagEntry} from "@perfice/model/journal/journal"; 3 | import {isTimestampInRange, type TimeRange} from "@perfice/model/variable/time/time"; 4 | 5 | export interface DateSearch { 6 | range: TimeRange; 7 | } 8 | 9 | export class DateSearchDefinition implements SearchDefinition<DateSearch> { 10 | matchesJournalEntry(search: DateSearch, _dependencies: SearchDependencies, entry: JournalEntry): boolean { 11 | return isTimestampInRange(entry.timestamp, search.range); 12 | } 13 | 14 | matchesTagEntry(search: DateSearch, _dependencies: SearchDependencies, entry: TagEntry): boolean { 15 | return isTimestampInRange(entry.timestamp, search.range); 16 | } 17 | 18 | getDefaultSearchMode(): SearchEntityMode { 19 | return SearchEntityMode.MUST_MATCH; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/model/journal/search/freeText.ts: -------------------------------------------------------------------------------- 1 | import {type SearchDefinition, type SearchDependencies, SearchEntityMode} from "@perfice/model/journal/search/search"; 2 | import type {JournalEntry, TagEntry} from "../journal"; 3 | import {primitiveAsString} from "@perfice/model/primitive/primitive"; 4 | 5 | export interface FreeTextSearch { 6 | search: string; 7 | } 8 | 9 | export class FreeTextSearchDefinition implements SearchDefinition<FreeTextSearch> { 10 | matchesJournalEntry(search: FreeTextSearch, _dependencies: SearchDependencies, entry: JournalEntry): boolean { 11 | if (entry.displayValue.toLowerCase().includes(search.search)) 12 | return true; 13 | 14 | // Returns true when any of the answers contains the search string 15 | return Object.values(entry.answers) 16 | .some(v => primitiveAsString(v).toLowerCase().includes(search.search)); 17 | } 18 | 19 | matchesTagEntry(search: FreeTextSearch, dependencies: SearchDependencies, entry: TagEntry): boolean { 20 | let tag = dependencies.tags.get(entry.tagId); 21 | if (tag == null) return false; 22 | 23 | return tag.name.toLowerCase().includes(search.search); 24 | } 25 | 26 | getDefaultSearchMode(): SearchEntityMode { 27 | return SearchEntityMode.MUST_MATCH; 28 | } 29 | } -------------------------------------------------------------------------------- /src/model/journal/search/ui.ts: -------------------------------------------------------------------------------- 1 | import type { Form } from "@perfice/model/form/form"; 2 | import type { Tag, TagCategory } from "@perfice/model/tag/tag"; 3 | import type { Trackable, TrackableCategory } from "@perfice/model/trackable/trackable"; 4 | 5 | export interface JournalSearchUiDependencies { 6 | forms: Form[]; 7 | trackables: Trackable[]; 8 | tags: Tag[]; 9 | trackableCategories: TrackableCategory[]; 10 | tagCategories: TagCategory[]; 11 | } 12 | -------------------------------------------------------------------------------- /src/model/notification/notification.ts: -------------------------------------------------------------------------------- 1 | import {Weekday} from "@capacitor/local-notifications"; 2 | 3 | export enum NotificationType { 4 | REFLECTION = "REFLECTION" 5 | } 6 | 7 | export const NOTIFICATION_WEEKDAYS: { name: string, value: number | null }[] = [ 8 | { 9 | name: "Every day", 10 | value: null 11 | }, 12 | { 13 | name: "Sunday", 14 | value: Weekday.Sunday 15 | }, 16 | { 17 | name: "Monday", 18 | value: Weekday.Monday 19 | }, 20 | { 21 | name: "Tuesday", 22 | value: Weekday.Tuesday 23 | }, 24 | { 25 | name: "Wednesday", 26 | value: Weekday.Wednesday 27 | }, 28 | { 29 | name: "Thursday", 30 | value: Weekday.Thursday 31 | }, 32 | { 33 | name: "Friday", 34 | value: Weekday.Friday 35 | }, 36 | { 37 | name: "Saturday", 38 | value: Weekday.Saturday 39 | } 40 | ] 41 | 42 | export interface StoredNotification { 43 | id: string; 44 | type: NotificationType; 45 | nativeId: number; 46 | entityId: string; 47 | 48 | title: string; 49 | body: string; 50 | 51 | hour: number; 52 | minutes: number; 53 | weekDay: number | null; 54 | } -------------------------------------------------------------------------------- /src/model/sharedWidgets/table/table.ts: -------------------------------------------------------------------------------- 1 | import {type TextOrDynamic, type VariableTypeDef, VariableTypeName} from "@perfice/model/variable/variable"; 2 | import {ListVariableType} from "@perfice/services/variable/types/list"; 3 | import type {SimpleTimeScopeType} from "@perfice/model/variable/time/time"; 4 | 5 | export interface TableWidgetSettings { 6 | formId: string; 7 | prefix: TextOrDynamic[]; 8 | suffix: TextOrDynamic[]; 9 | timeScope: SimpleTimeScopeType; 10 | // Question id to optionally group by 11 | groupBy: string | null; 12 | } 13 | 14 | export function createTypeDefForTableWidget(settings: TableWidgetSettings): VariableTypeDef { 15 | let fields: Set<string> = new Set(); 16 | [...settings.prefix, ...settings.suffix] 17 | .filter(v => v.dynamic) 18 | .forEach(v => fields.add(v.value)); 19 | 20 | if (settings.groupBy != null) { 21 | fields.add(settings.groupBy); 22 | } 23 | 24 | return { 25 | type: VariableTypeName.LIST, 26 | value: new ListVariableType(settings.formId, 27 | Object.fromEntries(fields.entries().map(([key]) => [key, true])), []) 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/model/tag/suggestions.ts: -------------------------------------------------------------------------------- 1 | import tagSuggestionsAsset from '@perfice/assets/tag_suggestions.json?raw' 2 | 3 | export const TAG_SUGGESTIONS: TagSuggestionGroup[] = JSON.parse(tagSuggestionsAsset); 4 | 5 | export interface TagSuggestionGroup { 6 | name: string; 7 | suggestions: TagSuggestion[]; 8 | } 9 | 10 | export type TagSuggestion = { 11 | name: string; 12 | } -------------------------------------------------------------------------------- /src/model/tag/tag.ts: -------------------------------------------------------------------------------- 1 | export interface Tag { 2 | id: string; 3 | name: string; 4 | variableId: string; 5 | order: number; 6 | categoryId: string | null; 7 | } 8 | 9 | export const UNCATEGORIZED_TAG_CATEGORY_ID = ""; 10 | 11 | export interface TagCategory { 12 | id: string; 13 | name: string; 14 | order: number; 15 | } 16 | -------------------------------------------------------------------------------- /src/model/ui/button.ts: -------------------------------------------------------------------------------- 1 | export enum ButtonColor { 2 | GREEN, 3 | RED, 4 | WHITE 5 | } 6 | -------------------------------------------------------------------------------- /src/model/ui/context-menu.ts: -------------------------------------------------------------------------------- 1 | import type {IconDefinition} from "@fortawesome/free-solid-svg-icons"; 2 | 3 | export interface OpenedContextMenu { 4 | callback: CloseContextMenuCallback; 5 | initiator: HTMLElement; 6 | } 7 | 8 | export type CloseContextMenuCallback = () => void; 9 | let openedContextMenus: OpenedContextMenu[] = []; 10 | 11 | export function openContextMenu(callback: CloseContextMenuCallback, initiator: HTMLElement) { 12 | openedContextMenus.push({callback, initiator}); 13 | } 14 | 15 | export function removeContextMenuCallback(callback: CloseContextMenuCallback) { 16 | openedContextMenus = openedContextMenus.filter(menu => menu.callback != callback); 17 | } 18 | 19 | export function closeContextMenus(initiator: HTMLElement) { 20 | // Close all context menus that aren't opened by the initiator 21 | openedContextMenus 22 | .filter(menu => menu.initiator != initiator) 23 | .forEach((menu) => menu.callback()); 24 | openedContextMenus = []; 25 | } 26 | 27 | export interface ContextMenuButton { 28 | name: string; 29 | icon: IconDefinition | null; 30 | separated?: boolean; 31 | action: () => void; 32 | } 33 | -------------------------------------------------------------------------------- /src/model/ui/dropdown.ts: -------------------------------------------------------------------------------- 1 | import type {IconDefinition} from "@fortawesome/free-solid-svg-icons"; 2 | 3 | export interface DropdownMenuItemDetails<T> { 4 | name: string; 5 | value: T; 6 | icon?: IconDefinition; 7 | separated?: boolean; 8 | } 9 | 10 | export type DropdownMenuItem<T> = DropdownMenuItemDetails<T> & { 11 | action?: () => void; 12 | } 13 | 14 | export const DROPDOWN_BUTTON_HEIGHT = 40; 15 | -------------------------------------------------------------------------------- /src/model/ui/dynamicInput.ts: -------------------------------------------------------------------------------- 1 | export interface DynamicInputEntity { 2 | id: string; 3 | type: string; 4 | name: string; 5 | fields: DynamicInputField[]; 6 | } 7 | 8 | export interface DynamicInputField { 9 | id: string; 10 | name: string; 11 | nested?: boolean; 12 | fields?: DynamicInputField[]; 13 | } 14 | 15 | export interface DynamicInputAnswer { 16 | id: string; 17 | type: string; 18 | name: string; 19 | answers: string[]; 20 | } 21 | -------------------------------------------------------------------------------- /src/model/ui/router.svelte.ts: -------------------------------------------------------------------------------- 1 | import {goto} from "@mateothegreat/svelte5-router"; 2 | 3 | export const routingNavigatorState = $state<string[]>([]); 4 | 5 | export function getCurrentRoute(state: string[]) { 6 | return state.length > 0 ? state[state.length - 1] : "/"; 7 | } -------------------------------------------------------------------------------- /src/model/ui/segmented.ts: -------------------------------------------------------------------------------- 1 | import type {IconDefinition} from "@fortawesome/free-solid-svg-icons"; 2 | 3 | export interface SegmentedItem<T> { 4 | name: string; 5 | value?: T; 6 | prefix?: IconDefinition; 7 | suffix?: IconDefinition; 8 | onClick?: () => void; 9 | } 10 | -------------------------------------------------------------------------------- /src/model/ui/sidebar.ts: -------------------------------------------------------------------------------- 1 | import { 2 | faBook, 3 | faBullseye, 4 | faCog, 5 | faHome, 6 | faLineChart, 7 | faSquarePlus, 8 | faSun, 9 | faTags, 10 | type IconDefinition 11 | } from "@fortawesome/free-solid-svg-icons"; 12 | 13 | export interface SidebarLink { 14 | icon: IconDefinition, 15 | path: string, 16 | title: string, 17 | showOnMobile?: boolean 18 | bottom?: boolean 19 | } 20 | 21 | export const SIDEBAR_LINKS: SidebarLink[] = [ 22 | {icon: faHome, path: "/", title: "Home"}, 23 | {icon: faSquarePlus, path: "/trackables", title: "Track"}, 24 | {icon: faBook, path: "/journal", title: "Journal"}, 25 | {icon: faBullseye, path: "/goals", title: "Goals"}, 26 | {icon: faTags, path: "/tags", title: "Tags"}, 27 | {icon: faLineChart, path: "/analytics", title: "Analytics", showOnMobile: false}, 28 | {icon: faSun, path: "/reflections", title: "Reflections", showOnMobile: false}, 29 | {icon: faCog, path: "/settings", title: "Settings", bottom: true}, 30 | ]; 31 | -------------------------------------------------------------------------------- /src/services/deletion/deletion.ts: -------------------------------------------------------------------------------- 1 | import type {Table} from "dexie"; 2 | 3 | export class DeletionService { 4 | 5 | private readonly tables: Record<string, Table>; 6 | 7 | constructor(tables: Record<string, Table>) { 8 | this.tables = tables; 9 | } 10 | 11 | async deleteAllData() { 12 | localStorage.clear(); 13 | for (let table of Object.values(this.tables)) { 14 | await table.clear(); 15 | } 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /src/services/form/template.ts: -------------------------------------------------------------------------------- 1 | import type {FormTemplateCollection} from "@perfice/db/collections"; 2 | import type {FormTemplate} from "@perfice/model/form/form"; 3 | import type {PrimitiveValue} from "@perfice/model/primitive/primitive"; 4 | 5 | export class FormTemplateService { 6 | 7 | private collection: FormTemplateCollection; 8 | 9 | constructor(collection: FormTemplateCollection) { 10 | this.collection = collection; 11 | } 12 | 13 | async createTemplate(formId: string, templateName: string, answers: Record<string, PrimitiveValue>) { 14 | await this.collection.createFormTemplate({ 15 | id: crypto.randomUUID(), 16 | formId, 17 | name: templateName, 18 | answers 19 | }); 20 | } 21 | 22 | 23 | async updateTemplate(template: FormTemplate, templateName: string, answers: Record<string, PrimitiveValue>) { 24 | await this.collection.updateFormTemplate({ 25 | ...template, 26 | name: templateName, 27 | answers 28 | }); 29 | } 30 | 31 | async getTemplatesByFormId(formId: string): Promise<FormTemplate[]> { 32 | return this.collection.getTemplatesByFormId(formId); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/services/notification/web.ts: -------------------------------------------------------------------------------- 1 | import type {NotificationScheduler} from "@perfice/services/notification/notification"; 2 | import type {StoredNotification} from "@perfice/model/notification/notification"; 3 | 4 | export class WebNotificationScheduler implements NotificationScheduler { 5 | 6 | async scheduleStoredNotifications(stored: StoredNotification[]) { 7 | 8 | } 9 | 10 | async scheduleNotification(notification: StoredNotification) { 11 | 12 | } 13 | 14 | async unscheduleNotification(nativeId: number) { 15 | 16 | } 17 | } -------------------------------------------------------------------------------- /src/services/observer.ts: -------------------------------------------------------------------------------- 1 | 2 | export enum EntityObserverType { 3 | CREATED, 4 | UPDATED, 5 | DELETED, 6 | ANY, 7 | } 8 | 9 | export type EntityObserverCallback<T> = (e: T) => Promise<void>; 10 | export interface EntityObserver<T> { 11 | type: EntityObserverType; 12 | callback: EntityObserverCallback<T>; 13 | } 14 | 15 | export class EntityObservers<T> { 16 | private observers: EntityObserver<T>[] = []; 17 | 18 | addObserver(type: EntityObserverType, callback: EntityObserverCallback<T>) { 19 | this.observers.push({ type, callback }); 20 | } 21 | 22 | removeObserver(type: EntityObserverType, callback: EntityObserverCallback<T>) { 23 | this.observers = this.observers.filter(o => o.type != type || o.callback != callback); 24 | } 25 | 26 | async notifyObservers(type: EntityObserverType, entity: T) { 27 | let observers = this.observers 28 | .filter(o => o.type == EntityObserverType.ANY || o.type == type); 29 | 30 | for (const observer of observers) { 31 | await observer.callback(entity); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/stores/analytics/settings.ts: -------------------------------------------------------------------------------- 1 | import {AsyncStore} from "@perfice/stores/store"; 2 | import {EntityObserverType} from "@perfice/services/observer"; 3 | import type {AnalyticsSettingsService} from "@perfice/services/analytics/settings"; 4 | import type {AnalyticsSettings} from "@perfice/model/analytics/analytics"; 5 | 6 | export class AnalyticsSettingsStore extends AsyncStore<AnalyticsSettings[]> { 7 | 8 | private service: AnalyticsSettingsService; 9 | 10 | constructor(service: AnalyticsSettingsService) { 11 | super(service.getAllSettings()); 12 | this.service = service; 13 | this.service.addObserver(EntityObserverType.CREATED, 14 | async (tag) => this.onAnalyticsSettingsCreated(tag)); 15 | this.service.addObserver(EntityObserverType.UPDATED, 16 | async (tag) => this.onAnalyticsSettingsUpdated(tag)); 17 | } 18 | 19 | async updateSettings(settings: AnalyticsSettings) { 20 | await this.service.updateSettings(settings); 21 | } 22 | 23 | private onAnalyticsSettingsCreated(settings: AnalyticsSettings) { 24 | this.updateResolved(v => [...v, settings]); 25 | } 26 | 27 | private onAnalyticsSettingsUpdated(settings: AnalyticsSettings) { 28 | this.updateResolved(v => 29 | v.map(prev => prev.formId == settings.formId ? settings : prev)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/stores/dashboard/widget/goal.ts: -------------------------------------------------------------------------------- 1 | import {WeekStart} from "@perfice/model/variable/time/time"; 2 | import {derived, type Readable} from "svelte/store"; 3 | import type {DashboardGoalWidgetSettings} from "@perfice/model/dashboard/widgets/goal"; 4 | import type {GoalValueResult} from "@perfice/stores/goal/value"; 5 | import type {Goal} from "@perfice/model/goal/goal"; 6 | import {goals, goalValue} from "@perfice/stores"; 7 | 8 | export interface GoalWidgetResult { 9 | goal: Goal; 10 | value: GoalValueResult; 11 | } 12 | 13 | export function GoalWidget(settings: DashboardGoalWidgetSettings, date: Date, 14 | weekStart: WeekStart, key: string): Readable<Promise<GoalWidgetResult>> { 15 | return derived(goalValue(settings.goalVariableId, settings.goalStreakVariableId, date, weekStart, key), (val, set) => { 16 | set(new Promise(async (resolve) => { 17 | let goal = await goals.getGoalByVariableId(settings.goalVariableId, true); 18 | if (goal == null) return; 19 | 20 | let value = await val; 21 | resolve({goal, value}); 22 | })); 23 | }); 24 | } -------------------------------------------------------------------------------- /src/stores/dashboard/widget/trackable.ts: -------------------------------------------------------------------------------- 1 | import {type Readable, writable} from "svelte/store"; 2 | import type {DashboardTrackableWidgetSettings} from "@perfice/model/dashboard/widgets/trackable"; 3 | import type {Trackable} from "@perfice/model/trackable/trackable"; 4 | import {trackables} from "@perfice/stores"; 5 | 6 | export interface TrackableWidgetResult { 7 | trackable: Trackable; 8 | } 9 | 10 | export function TrackableWidget(settings: DashboardTrackableWidgetSettings): Readable<Promise<TrackableWidgetResult>> { 11 | return writable(new Promise(async (resolve) => { 12 | let trackable = await trackables.getTrackableById(settings.trackableId, true); 13 | if (trackable == null) return; 14 | 15 | resolve({trackable}); 16 | })); 17 | } -------------------------------------------------------------------------------- /src/stores/deletion/deletion.ts: -------------------------------------------------------------------------------- 1 | import type {DeletionService} from "@perfice/services/deletion/deletion"; 2 | 3 | export class DeletionStore { 4 | 5 | private readonly deletionService: DeletionService; 6 | 7 | constructor(deletionService: DeletionService) { 8 | this.deletionService = deletionService; 9 | } 10 | 11 | async deleteAllData() { 12 | await this.deletionService.deleteAllData(); 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /src/stores/export/complete.ts: -------------------------------------------------------------------------------- 1 | import type {CompleteExportService} from "@perfice/services/export/complete/complete"; 2 | 3 | export class CompleteExportStore { 4 | 5 | private readonly exportService: CompleteExportService; 6 | 7 | constructor(exportService: CompleteExportService) { 8 | this.exportService = exportService; 9 | } 10 | 11 | async export() { 12 | return await this.exportService.export(); 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /src/stores/export/formEntry.ts: -------------------------------------------------------------------------------- 1 | import type {EntryExportService} from "@perfice/services/export/formEntries/export"; 2 | 3 | export class EntryExportStore { 4 | 5 | private readonly exportService: EntryExportService; 6 | 7 | constructor(exportService: EntryExportService) { 8 | this.exportService = exportService; 9 | } 10 | 11 | async exportJson(formId: string) { 12 | return await this.exportService.exportJson(formId); 13 | } 14 | 15 | async exportCsv(formId: string) { 16 | return await this.exportService.exportCsv(formId); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/stores/import/complete.ts: -------------------------------------------------------------------------------- 1 | import type {CompleteImportService} from "@perfice/services/import/complete/complete"; 2 | import {BASE_URL} from "@perfice/app"; 3 | 4 | export class CompleteImportStore { 5 | 6 | private readonly importService: CompleteImportService; 7 | 8 | constructor(importService: CompleteImportService) { 9 | this.importService = importService; 10 | } 11 | 12 | async import(file: File, newFormat: boolean) { 13 | await this.importService.import(file, newFormat); 14 | window.location.href = BASE_URL + "/"; 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /src/stores/journal/tag.ts: -------------------------------------------------------------------------------- 1 | import type {TagEntry} from "@perfice/model/journal/journal"; 2 | import {AsyncStore} from "@perfice/stores/store"; 3 | import type {TagEntryService} from "@perfice/services/tag/entry"; 4 | import {emptyPromise} from "@perfice/util/promise"; 5 | 6 | export class TagEntryStore extends AsyncStore<TagEntry[]> { 7 | 8 | private tagEntryService: TagEntryService; 9 | 10 | constructor(tagEntryService: TagEntryService) { 11 | super(emptyPromise()); 12 | this.tagEntryService = tagEntryService; 13 | } 14 | 15 | async init() { 16 | this.setResolved([]); 17 | } 18 | 19 | async nextPage(page: number, size: number): Promise<TagEntry[]> { 20 | return await this.tagEntryService.getEntriesUntilTimeAndLimit(page, size); 21 | } 22 | 23 | async deleteEntryById(id: string) { 24 | await this.tagEntryService.deleteEntryById(id); 25 | this.updateResolved(v => v.filter(e => e.id != id)); 26 | } 27 | } -------------------------------------------------------------------------------- /src/stores/tag/categorized.ts: -------------------------------------------------------------------------------- 1 | import {derived, type Readable} from "svelte/store"; 2 | import {categorize, type CategoryList} from "@perfice/util/category"; 3 | import type {Tag, TagCategory} from "@perfice/model/tag/tag"; 4 | import {tagCategories, tags} from "@perfice/stores"; 5 | 6 | export function CategorizedTags(): Readable<Promise<CategoryList<TagCategory, Tag>[]>> { 7 | return derived<[Readable<Promise<Tag[]>>, Readable<Promise<TagCategory[]>>], 8 | Promise<CategoryList<TagCategory, Tag>[]>> 9 | 10 | ([tags, tagCategories], ([$tags, $categories], set) => { 11 | let promise = new Promise<CategoryList<TagCategory, Tag>[]>( 12 | async (resolve) => { 13 | let tags = await $tags; 14 | let categories = (await $categories).sort((a, b) => a.order - b.order); 15 | 16 | let res = categorize(categories, tags); 17 | 18 | for (let category of res) { 19 | category.items = category.items.sort((a, b) => 20 | a.order - b.order); 21 | } 22 | 23 | resolve(res); 24 | }); 25 | set(promise); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /src/stores/trackable/categorized.ts: -------------------------------------------------------------------------------- 1 | import type {Trackable, TrackableCategory} from "@perfice/model/trackable/trackable"; 2 | import {categorize, type CategoryList} from "@perfice/util/category"; 3 | import {derived, type Readable} from "svelte/store"; 4 | import {trackableCategories, trackables} from "@perfice/stores"; 5 | 6 | export function CategorizedTrackables(): Readable<Promise<CategoryList<TrackableCategory, Trackable>[]>> { 7 | return derived<[Readable<Promise<Trackable[]>>, Readable<Promise<TrackableCategory[]>>], 8 | Promise<CategoryList<TrackableCategory, Trackable>[]>> 9 | 10 | ([trackables, trackableCategories], ([$trackables, $categories], set) => { 11 | let promise = new Promise<CategoryList<TrackableCategory, Trackable>[]>( 12 | async (resolve) => { 13 | let trackables = await $trackables; 14 | let categories = (await $categories) 15 | .sort((a, b) => a.order - b.order); 16 | 17 | let res = categorize(categories, trackables); 18 | 19 | for (let category of res) { 20 | category.items = category.items.sort((a, b) => 21 | a.order - b.order); 22 | } 23 | 24 | resolve(res); 25 | }); 26 | set(promise); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /src/stores/ui/drawer.ts: -------------------------------------------------------------------------------- 1 | import {writable} from "svelte/store"; 2 | 3 | export const drawerOpen = writable(false); 4 | 5 | export function closeDrawer() { 6 | drawerOpen.set(false); 7 | } 8 | 9 | export function toggleDrawer() { 10 | drawerOpen.update(v => !v); 11 | } 12 | -------------------------------------------------------------------------------- /src/stores/ui/weekStart.ts: -------------------------------------------------------------------------------- 1 | import {CustomStore} from "@perfice/stores/store"; 2 | import {WeekStart} from "@perfice/model/variable/time/time"; 3 | 4 | const WEEK_START_STORAGE_KEY = "week_start"; 5 | 6 | export function loadStoredWeekStart(): WeekStart { 7 | let weekStartStr = localStorage.getItem(WEEK_START_STORAGE_KEY); 8 | if (weekStartStr != null) { 9 | let value = parseInt(weekStartStr); 10 | if (isFinite(value)) { 11 | return value as WeekStart; 12 | } 13 | } 14 | 15 | let defaultValue = WeekStart.MONDAY; 16 | localStorage.setItem(WEEK_START_STORAGE_KEY, defaultValue.toString()); 17 | return defaultValue; 18 | } 19 | 20 | export type WeekStartObserver = (weekStart: WeekStart) => void; 21 | 22 | export class WeekStartStore extends CustomStore<WeekStart> { 23 | 24 | private observers: WeekStartObserver[] = []; 25 | 26 | constructor(weekStart: WeekStart) { 27 | super(weekStart); 28 | } 29 | 30 | setWeekStart(weekStart: WeekStart) { 31 | this.set(weekStart); 32 | localStorage.setItem(WEEK_START_STORAGE_KEY, weekStart.toString()); 33 | this.observers.forEach(o => o(weekStart)); 34 | } 35 | 36 | addObserver(observer: WeekStartObserver) { 37 | this.observers.push(observer); 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /src/swSetup.ts: -------------------------------------------------------------------------------- 1 | import {BASE_URL} from "@perfice/app"; 2 | 3 | export function setupServiceWorker() { 4 | if ("serviceWorker" in navigator) { 5 | let registration = navigator.serviceWorker 6 | .register( 7 | BASE_URL + "/sw.js", 8 | // import.meta.env.MODE === "production" 9 | // ? "/app/sw.js" 10 | // : "/dev-sw.js?dev-sw", 11 | // {type: import.meta.env.MODE === 'production' ? 'classic' : 'module'}, 12 | ) 13 | .then((r) => { 14 | }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/util/array.ts: -------------------------------------------------------------------------------- 1 | export interface Identified<T> { 2 | id: T 3 | } 4 | 5 | export function updateKeyedInArray<V, K>(v: V[], keyFinder: (v: V, k: K) => boolean, key: K, update: V) { 6 | return v.map(previous => keyFinder(previous, key) == keyFinder(update, key) ? update : previous); 7 | } 8 | 9 | export function updateIdentifiedInArray<V extends Identified<K>, K>(v: V[], update: V) { 10 | return updateKeyedInArray(v, (val, key) => val.id == key, update.id, update); 11 | } 12 | 13 | export function deleteIdentifiedInArray<V extends Identified<K>, K>(v: V[], id: K): V[] { 14 | return v.filter(val => val.id != id); 15 | } 16 | 17 | export function updateIndexInArray<V>(v: V[], index: number, update: V) { 18 | return v.map((val, i) => i == index ? update : val); 19 | } 20 | 21 | export function findArrayDifferences<T>(originalList: T[], updatedList: T[]) { 22 | const added = updatedList.filter(item => !originalList.includes(item)); 23 | const removed = originalList.filter(item => !updatedList.includes(item)); 24 | 25 | return {added, removed}; 26 | } 27 | 28 | export function reorderGeneric<T extends { order: number }>(items: T[]): T[] { 29 | let values: T[] = []; 30 | for (let i = 0; i < items.length; i++) { 31 | values.push({...items[i], order: i}); 32 | } 33 | 34 | return values; 35 | } 36 | -------------------------------------------------------------------------------- /src/util/category.ts: -------------------------------------------------------------------------------- 1 | import type {Identified} from "@perfice/util/array"; 2 | 3 | export const UNCATEGORIZED_NAME = "Uncategorized"; 4 | 5 | export interface CategoryList<C, T> { 6 | category: C | null; 7 | items: T[]; 8 | } 9 | 10 | export interface CategoryItem { 11 | categoryId: string | null; 12 | } 13 | 14 | export function categorize<C extends Identified<string>, T extends CategoryItem>(categories: C[], items: T[]): CategoryList<C, T>[] { 15 | let uncategorized: CategoryList<C, T> = 16 | { 17 | category: null, 18 | items: [] 19 | }; 20 | 21 | let res: CategoryList<C, T>[] = [ 22 | // Category for uncategorized items 23 | uncategorized, 24 | ]; 25 | 26 | for (let category of categories) { 27 | // Add a category list for each category 28 | res.push({category, items: []}); 29 | } 30 | 31 | for (let item of items) { 32 | // Group each item by category 33 | let category = res.find(c => c.category?.id == item.categoryId 34 | || (item.categoryId == null && c.category == null)); 35 | 36 | if (category == null) continue; 37 | 38 | category.items.push(item); 39 | } 40 | 41 | return res; 42 | } 43 | -------------------------------------------------------------------------------- /src/util/event.ts: -------------------------------------------------------------------------------- 1 | import {type Writable} from "svelte/store"; 2 | 3 | export function subscribeToEventStore<T>(list: T[], writable: Writable<T[]>, handler: (e: T) => void) { 4 | if(list.length == 0) return; 5 | 6 | for(let event of list) { 7 | handler(event); 8 | } 9 | 10 | writable.set([]); 11 | } 12 | 13 | export function publishToEventStore<T>(writable: Writable<T[]>, event: T) { 14 | writable.update(v => [...v, event]); 15 | } 16 | -------------------------------------------------------------------------------- /src/util/file.ts: -------------------------------------------------------------------------------- 1 | import {Capacitor} from "@capacitor/core"; 2 | 3 | import {Share} from '@capacitor/share'; 4 | import {Directory, Encoding, Filesystem} from '@capacitor/filesystem'; 5 | 6 | export async function downloadTextFile(fileName: string, mimeType: string, text: string) { 7 | if (Capacitor.isNativePlatform()) { 8 | const path = `${Date.now()}-${fileName}`; 9 | 10 | await Filesystem.writeFile({ 11 | path, 12 | data: text, 13 | encoding: Encoding.UTF8, 14 | directory: Directory.Cache, 15 | }); 16 | 17 | const fileUri = await Filesystem.getUri({ 18 | path, 19 | directory: Directory.Cache, 20 | }); 21 | 22 | await Share.share({ 23 | title: 'Export file', 24 | url: fileUri.uri, 25 | dialogTitle: 'Save or share your export file', 26 | }); 27 | } else { 28 | const blob = new Blob([text], {type: mimeType}); 29 | const url = URL.createObjectURL(blob); 30 | const a = document.createElement("a"); 31 | a.href = url; 32 | a.download = fileName; 33 | document.body.appendChild(a); 34 | a.click(); 35 | document.body.removeChild(a); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/util/local.ts: -------------------------------------------------------------------------------- 1 | export function parseJsonFromLocalStorage<T>(key: string): T | null { 2 | try { 3 | let raw = localStorage.getItem(key); 4 | if (raw == null) return null; 5 | 6 | return JSON.parse(raw); 7 | } catch (e) { 8 | return null; 9 | } 10 | } -------------------------------------------------------------------------------- /src/util/math.ts: -------------------------------------------------------------------------------- 1 | export function calculateProgressSafe(first: number, total: number): number { 2 | if (total == 0) return 0; 3 | 4 | return first / total; 5 | } 6 | 7 | export function numberToMaxDecimals(value: number, decimals: number): string { 8 | return Number.isInteger(value) ? value.toString() : value.toFixed(decimals) 9 | } -------------------------------------------------------------------------------- /src/util/perf.ts: -------------------------------------------------------------------------------- 1 | import {parseJsonFromLocalStorage} from "@perfice/util/local"; 2 | 3 | interface PerfEntry<D> { 4 | data: D; 5 | time: number; 6 | } 7 | 8 | let perf = parseJsonFromLocalStorage<PerfEntry<any>[]>("perf") ?? []; 9 | 10 | export function getPerfSummary() { 11 | let top = perf.sort((a, b) => b.time - a.time).slice(0, 10); 12 | return top.map(e => `${e.data}: ${e.time}ms`).join("\n"); 13 | } 14 | 15 | export function debugPerformance<T, D>(data: D, callback: () => T): T { 16 | let start = performance.now(); 17 | let value = callback(); 18 | let time = performance.now() - start; 19 | 20 | perf.push({ 21 | data, 22 | time 23 | }); 24 | 25 | localStorage.setItem("perf", JSON.stringify(perf)); 26 | return value; 27 | } -------------------------------------------------------------------------------- /src/util/promise.ts: -------------------------------------------------------------------------------- 1 | export function emptyPromise<T>(): Promise<T> { 2 | return new Promise<T>((_) => { 3 | }); 4 | } 5 | 6 | export function resolvedPromise<T>(v: T): Promise<T> { 7 | return new Promise<T>((resolve) => resolve(v)); 8 | } 9 | 10 | export function resolvedUpdatePromise<T>(promise: Promise<T>, updater: (v: T) => T): Promise<T> { 11 | return new Promise<T>(async (resolve) => { 12 | let existing = await promise; 13 | resolve(updater(existing)); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /src/util/window.ts: -------------------------------------------------------------------------------- 1 | export function isMobile() { 2 | return window.innerWidth < 768; 3 | } -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// <reference types="svelte" /> 2 | /// <reference types="vite/client" /> 3 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' 2 | 3 | export default { 4 | // Consult https://svelte.dev/docs#compile-time-svelte-preprocess 5 | // for more information about preprocessors 6 | preprocess: vitePreprocess(), 7 | } 8 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | import plugin from "tailwindcss/plugin"; 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | export default { 5 | content: [ 6 | "index.html", 7 | "./src/**/*.{svelte,js,ts,jsx,tsx}" 8 | ], 9 | theme: { 10 | extend: {}, 11 | }, 12 | plugins: [ 13 | plugin(function({ addVariant, e }) { 14 | addVariant('pointer-feedback', ['@media (pointer: fine) { &:hover }','@media (pointer: coarse) { &:active }']); 15 | }) 16 | , 17 | ], 18 | } 19 | 20 | -------------------------------------------------------------------------------- /tests/common.ts: -------------------------------------------------------------------------------- 1 | import {JournalEntry} from "../src/model/journal/journal"; 2 | import {pDisplay, PrimitiveValue, pString} from "../src/model/primitive/primitive"; 3 | 4 | export function mockEntry(id: string, formId: string, answers: Record<string, PrimitiveValue>, timestamp: number = 0): JournalEntry { 5 | return { 6 | id, 7 | formId, 8 | snapshotId: "", 9 | timestamp, 10 | displayValue: "", 11 | answers: Object.fromEntries(Object.entries(answers) 12 | .map(([k, v]) => [k, pDisplay(v, pString(v.value.toString()))])) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/primitive.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from "vitest"; 2 | import {comparePrimitives, pList, pString} from "../src/model/primitive/primitive"; 3 | 4 | test("string primitives equal", () => { 5 | expect(comparePrimitives(pString("test"), pString("test"))).toBeTruthy() 6 | }) 7 | 8 | test("string primitives inequal", () => { 9 | expect(comparePrimitives(pString("test"), pString("not the same"))).toBeFalsy() 10 | }) 11 | 12 | test("list primitives equal", () => { 13 | expect(comparePrimitives(pList([pString("test")]), pList([pString("test")]))).toBeTruthy() 14 | }) 15 | 16 | test("list primitives inequal", () => { 17 | expect(comparePrimitives(pList([pString("test")]), pList([pString("ok")]))).toBeFalsy() 18 | }) 19 | 20 | test("list primitives wrong order inequal", () => { 21 | expect(comparePrimitives(pList([pString("test"), pString("test2")]), 22 | pList([pString("test2"), pString("test")]))).toBeFalsy() 23 | }) 24 | -------------------------------------------------------------------------------- /tests/simple-time.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from "vitest"; 2 | import {dateToWeekEnd, dateToWeekStart} from "../src/util/time/simple"; 3 | import {WeekStart} from "../src/model/variable/time/time"; 4 | 5 | test("date to week start sunday", () => { 6 | let date = new Date(2025, 0, 16); 7 | expect(dateToWeekStart(date, WeekStart.SUNDAY).getTime()) 8 | .toEqual(Date.UTC(2025, 0, 12, date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds())) 9 | }) 10 | 11 | test("date to week start monday", () => { 12 | let date = new Date(Date.UTC(2025, 0, 16)); 13 | expect(dateToWeekStart(date, WeekStart.MONDAY).getTime()) 14 | .toEqual(Date.UTC(2025, 0, 13, date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds())) 15 | }) 16 | 17 | test("date to week end monday", () => { 18 | let date = new Date(Date.UTC(2025, 0, 16)); 19 | expect(dateToWeekEnd(date, WeekStart.MONDAY).getTime()) 20 | .toEqual(Date.UTC(2025, 0, 19, date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds())) 21 | }) 22 | 23 | -------------------------------------------------------------------------------- /tests/time-scope.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from "vitest"; 2 | import {dateToEndOfTimeScope, dateToStartOfTimeScope} from "../src/util/time/simple"; 3 | import {SimpleTimeScopeType, WeekStart} from "../src/model/variable/time/time"; 4 | 5 | test("date to start of week monday", () => { 6 | let date = new Date(2025, 0, 16); 7 | expect(dateToStartOfTimeScope(date, SimpleTimeScopeType.WEEKLY, WeekStart.MONDAY).getTime()) 8 | .toEqual(Date.UTC(2025, 0, 13, 0, 0, 0, 0)) 9 | }) 10 | 11 | test("date to end of week monday", () => { 12 | let date = new Date(2025, 0, 16); 13 | expect(dateToEndOfTimeScope(date, SimpleTimeScopeType.WEEKLY, WeekStart.MONDAY).getTime()) 14 | .toEqual(Date.UTC(2025, 0, 19, 23, 59, 59, 999)) 15 | }) 16 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "resolveJsonModule": true, 8 | /** 9 | * Typecheck JS in `.svelte` and `.js` files by default. 10 | * Disable checkJs if you'd like to use dynamic types in JS. 11 | * Note that setting allowJs false does not prevent the use 12 | * of JS in `.svelte` files. 13 | */ 14 | "allowJs": true, 15 | "checkJs": true, 16 | "isolatedModules": true, 17 | "moduleDetection": "force", 18 | "paths": { 19 | "@perfice/*": ["./src/*"] 20 | } 21 | }, 22 | "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"] 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true, 22 | "paths": { 23 | "@perfice/*": ["./src/*"] 24 | } 25 | }, 26 | "include": ["vite.config.ts"] 27 | } 28 | --------------------------------------------------------------------------------