permission_forge 0.14.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +2372 -0
- data/README.md +76 -25
- data/app/controllers/concerns/permission_forge/api/crud_actions.rb +3 -2
- data/app/controllers/concerns/permission_forge/api/input_shapes.rb +200 -0
- data/app/controllers/concerns/permission_forge/api/pagination.rb +12 -5
- data/app/controllers/concerns/permission_forge/api/response_renderer.rb +37 -1
- data/app/controllers/concerns/permission_forge/api_authentication.rb +13 -2
- data/app/controllers/concerns/permission_forge/cache_confirmation.rb +42 -11
- data/app/controllers/concerns/permission_forge/change_review.rb +42 -0
- data/app/controllers/concerns/permission_forge/copy_refusal.rb +39 -0
- data/app/controllers/concerns/permission_forge/param_shapes.rb +207 -7
- data/app/controllers/concerns/permission_forge/save_notice.rb +69 -0
- data/app/controllers/permission_forge/api/audit_events_controller.rb +1 -1
- data/app/controllers/permission_forge/api/base_controller.rb +163 -12
- data/app/controllers/permission_forge/api/dashboard_controller.rb +29 -4
- data/app/controllers/permission_forge/api/permission_set_group_users_controller.rb +5 -2
- data/app/controllers/permission_forge/api/permission_set_groups_controller.rb +5 -4
- data/app/controllers/permission_forge/api/permission_set_rules_controller.rb +7 -5
- data/app/controllers/permission_forge/api/permission_set_users_controller.rb +80 -19
- data/app/controllers/permission_forge/api/permission_sets_controller.rb +12 -6
- data/app/controllers/permission_forge/api/resource_actions_controller.rb +1 -1
- data/app/controllers/permission_forge/api/resource_fields_controller.rb +3 -4
- data/app/controllers/permission_forge/api/resources_controller.rb +26 -12
- data/app/controllers/permission_forge/api/unmatched_routes_controller.rb +20 -0
- data/app/controllers/permission_forge/application_controller.rb +39 -5
- data/app/controllers/permission_forge/assignments_controller.rb +36 -34
- data/app/controllers/permission_forge/bulk_assignments_controller.rb +8 -4
- data/app/controllers/permission_forge/field_lookups_controller.rb +1 -1
- data/app/controllers/permission_forge/maintenance_controller.rb +6 -2
- data/app/controllers/permission_forge/people_controller.rb +14 -14
- data/app/controllers/permission_forge/permission_set_editor.rb +20 -6
- data/app/controllers/permission_forge/permission_set_group_users_controller.rb +3 -1
- data/app/controllers/permission_forge/permission_set_groups_controller.rb +23 -9
- data/app/controllers/permission_forge/permission_set_rules_controller.rb +20 -10
- data/app/controllers/permission_forge/permission_sets_controller.rb +23 -24
- data/app/controllers/permission_forge/resource_actions_controller.rb +11 -6
- data/app/controllers/permission_forge/resource_fields_controller.rb +37 -19
- data/app/controllers/permission_forge/resources_controller.rb +12 -11
- data/app/frontend/permission_forge/pf_admin.css +99 -17
- data/app/frontend/permission_forge/pf_admin.js +36 -0
- data/app/helpers/permission_forge/application_helper.rb +22 -72
- data/app/helpers/permission_forge/audit_attribution_helper.rb +77 -0
- data/app/helpers/permission_forge/audit_helper.rb +57 -119
- data/app/helpers/permission_forge/audit_records_helper.rb +103 -0
- data/app/helpers/permission_forge/audit_sentences_helper.rb +56 -2
- data/app/helpers/permission_forge/badges_helper.rb +74 -17
- data/app/helpers/permission_forge/changes_helper.rb +24 -6
- data/app/helpers/permission_forge/conditions_helper.rb +119 -0
- data/app/helpers/permission_forge/labels_helper.rb +134 -0
- data/app/helpers/permission_forge/people_helper.rb +11 -19
- data/app/helpers/permission_forge/rule_problems_helper.rb +73 -0
- data/app/helpers/permission_forge/sentences_helper.rb +119 -98
- data/app/helpers/permission_forge/settings_helper.rb +2 -2
- data/app/helpers/permission_forge/ui_helper.rb +65 -4
- data/app/models/permission_forge/audit_event.rb +5 -1
- data/app/models/permission_forge/permission_set.rb +208 -18
- data/app/models/permission_forge/permission_set_group.rb +10 -4
- data/app/models/permission_forge/permission_set_group_user.rb +3 -1
- data/app/models/permission_forge/permission_set_rule.rb +254 -17
- data/app/models/permission_forge/permission_set_user.rb +38 -3
- data/app/models/permission_forge/resource.rb +18 -8
- data/app/models/permission_forge/resource_action.rb +15 -6
- data/app/models/permission_forge/resource_field.rb +97 -11
- data/app/views/permission_forge/assignments/confirm_destroy.html.erb +4 -4
- data/app/views/permission_forge/assignments/new.html.erb +10 -6
- data/app/views/permission_forge/audit_events/index.html.erb +1 -1
- data/app/views/permission_forge/audit_events/show.html.erb +16 -9
- data/app/views/permission_forge/bulk_assignments/confirm_revoke.html.erb +3 -3
- data/app/views/permission_forge/bulk_assignments/new.html.erb +3 -3
- data/app/views/permission_forge/field_lookups/show.html.erb +2 -2
- data/app/views/permission_forge/home/_broken_rules_card.html.erb +35 -0
- data/app/views/permission_forge/home/_conditions_card.html.erb +28 -0
- data/app/views/permission_forge/home/_ending_card.html.erb +11 -5
- data/app/views/permission_forge/home/_expired_card.html.erb +1 -1
- data/app/views/permission_forge/home/_mask_strategies_card.html.erb +27 -0
- data/app/views/permission_forge/home/_privacy_card.html.erb +21 -7
- data/app/views/permission_forge/home/_rules_card.html.erb +1 -1
- data/app/views/permission_forge/home/_secret_rules_card.html.erb +33 -0
- data/app/views/permission_forge/home/_stale_card.html.erb +7 -5
- data/app/views/permission_forge/home/_tidy_card.html.erb +1 -1
- data/app/views/permission_forge/home/index.html.erb +5 -6
- data/app/views/permission_forge/people/_access_tab.html.erb +26 -19
- data/app/views/permission_forge/people/_audit_tab.html.erb +8 -8
- data/app/views/permission_forge/people/_effective_field_source.html.erb +13 -7
- data/app/views/permission_forge/people/_effective_tab.html.erb +16 -15
- data/app/views/permission_forge/people/_effective_why.html.erb +20 -16
- data/app/views/permission_forge/people/_why_item.html.erb +1 -1
- data/app/views/permission_forge/people/confirm_remove_all.html.erb +3 -3
- data/app/views/permission_forge/people/erasure_preview.html.erb +1 -1
- data/app/views/permission_forge/permission_set_groups/confirm_destroy.html.erb +3 -3
- data/app/views/permission_forge/permission_set_groups/index.html.erb +3 -3
- data/app/views/permission_forge/permission_set_groups/show.html.erb +28 -19
- data/app/views/permission_forge/permission_set_rules/_builder.html.erb +35 -15
- data/app/views/permission_forge/permission_set_rules/_preview.html.erb +17 -0
- data/app/views/permission_forge/permission_set_rules/confirm_destroy.html.erb +3 -3
- data/app/views/permission_forge/permission_set_rules/preview.html.erb +12 -0
- data/app/views/permission_forge/permission_sets/_assignments_tab.html.erb +37 -22
- data/app/views/permission_forge/permission_sets/_audit_tab.html.erb +4 -4
- data/app/views/permission_forge/permission_sets/_condition_problems.html.erb +15 -0
- data/app/views/permission_forge/permission_sets/_conditions_tab.html.erb +14 -8
- data/app/views/permission_forge/permission_sets/_header.html.erb +6 -5
- data/app/views/permission_forge/permission_sets/_records_summary.html.erb +2 -2
- data/app/views/permission_forge/permission_sets/_resources_tab.html.erb +4 -4
- data/app/views/permission_forge/permission_sets/confirm_destroy.html.erb +4 -4
- data/app/views/permission_forge/permission_sets/confirm_switch_mode.html.erb +3 -3
- data/app/views/permission_forge/permission_sets/index.html.erb +6 -6
- data/app/views/permission_forge/permission_sets/new.html.erb +2 -2
- data/app/views/permission_forge/permission_sets/review.html.erb +13 -3
- data/app/views/permission_forge/permission_sets/show.html.erb +7 -5
- data/app/views/permission_forge/resource_actions/_across_sets.html.erb +7 -10
- data/app/views/permission_forge/resource_actions/_effect.html.erb +5 -5
- data/app/views/permission_forge/resource_actions/_form.html.erb +1 -1
- data/app/views/permission_forge/resource_actions/_managed_note.html.erb +5 -3
- data/app/views/permission_forge/resource_actions/confirm_destroy.html.erb +1 -1
- data/app/views/permission_forge/resource_actions/new.html.erb +1 -1
- data/app/views/permission_forge/resource_actions/show.html.erb +2 -2
- data/app/views/permission_forge/resource_fields/_explain.html.erb +19 -15
- data/app/views/permission_forge/resource_fields/show.html.erb +21 -12
- data/app/views/permission_forge/resources/_actions_tab.html.erb +4 -4
- data/app/views/permission_forge/resources/_fields_tab.html.erb +17 -9
- data/app/views/permission_forge/resources/_rules_tab.html.erb +9 -6
- data/app/views/permission_forge/resources/_sync.html.erb +8 -5
- data/app/views/permission_forge/resources/confirm_destroy.html.erb +3 -3
- data/app/views/permission_forge/resources/new.html.erb +3 -2
- data/app/views/permission_forge/resources/show.html.erb +6 -6
- data/app/views/permission_forge/settings/_row.html.erb +2 -2
- data/app/views/permission_forge/settings/show.html.erb +10 -1
- data/app/views/permission_forge/shared/_across_sets.html.erb +8 -9
- data/app/views/permission_forge/shared/_audit_events.html.erb +2 -2
- data/app/views/permission_forge/shared/_audit_footer.html.erb +13 -2
- data/app/views/permission_forge/shared/_crud_cell.html.erb +6 -6
- data/app/views/permission_forge/shared/_mode_banners.html.erb +1 -1
- data/app/views/permission_forge/shared/_savebar.html.erb +4 -2
- data/config/locales/permission_forge.en.yml +480 -69
- data/config/locales/permission_forge.mk.yml +499 -79
- data/config/locales/permission_forge.sq.yml +509 -80
- data/config/routes.rb +38 -23
- data/lib/generators/permission_forge/install/install_generator.rb +21 -0
- data/lib/generators/permission_forge/install/templates/initializer.rb.tt +35 -34
- data/lib/generators/permission_forge/install/templates/migration.rb.tt +5 -5
- data/lib/permission_forge/activation_conditions.rb +200 -0
- data/lib/permission_forge/admin/action_label.rb +33 -0
- data/lib/permission_forge/admin/action_rows.rb +3 -1
- data/lib/permission_forge/admin/actor_label.rb +52 -0
- data/lib/permission_forge/admin/attention_items.rb +152 -31
- data/lib/permission_forge/admin/audit_log.rb +4 -7
- data/lib/permission_forge/admin/audit_trail.rb +33 -17
- data/lib/permission_forge/admin/bulk_people_matcher.rb +32 -14
- data/lib/permission_forge/admin/chain_entries.rb +42 -0
- data/lib/permission_forge/admin/change_set.rb +12 -1
- data/lib/permission_forge/admin/change_set_parts/base.rb +5 -4
- data/lib/permission_forge/admin/change_set_parts/context.rb +3 -1
- data/lib/permission_forge/admin/change_set_parts/field_edit.rb +4 -1
- data/lib/permission_forge/admin/change_set_parts/field_levels.rb +1 -1
- data/lib/permission_forge/admin/change_set_parts/new_actions.rb +1 -1
- data/lib/permission_forge/admin/change_set_parts/new_fields.rb +38 -11
- data/lib/permission_forge/admin/change_set_parts/resource_edit.rb +17 -1
- data/lib/permission_forge/admin/change_set_parts/resource_flags.rb +1 -1
- data/lib/permission_forge/admin/change_set_parts/set_attrs.rb +4 -0
- data/lib/permission_forge/admin/change_set_parts/values.rb +10 -4
- data/lib/permission_forge/admin/column_fields.rb +22 -21
- data/lib/permission_forge/admin/conditions_form.rb +47 -15
- data/lib/permission_forge/admin/controller_catalog.rb +42 -14
- data/lib/permission_forge/admin/editable_lock.rb +83 -3
- data/lib/permission_forge/admin/effective_access.rb +147 -48
- data/lib/permission_forge/admin/effective_fields.rb +75 -67
- data/lib/permission_forge/admin/error_sentence.rb +47 -0
- data/lib/permission_forge/admin/expiry_presets.rb +17 -0
- data/lib/permission_forge/admin/exports/holdings.rb +6 -3
- data/lib/permission_forge/admin/field_across_sets.rb +7 -4
- data/lib/permission_forge/admin/field_chain.rb +109 -0
- data/lib/permission_forge/admin/field_detail.rb +24 -11
- data/lib/permission_forge/admin/field_label.rb +109 -0
- data/lib/permission_forge/admin/field_level.rb +4 -68
- data/lib/permission_forge/admin/field_provenance.rb +132 -0
- data/lib/permission_forge/admin/field_rows.rb +26 -11
- data/lib/permission_forge/admin/last_changes.rb +14 -0
- data/lib/permission_forge/admin/list_sentence.rb +32 -0
- data/lib/permission_forge/admin/mask_strategy_choices.rb +57 -0
- data/lib/permission_forge/admin/model_catalog.rb +83 -39
- data/lib/permission_forge/admin/model_label.rb +66 -0
- data/lib/permission_forge/admin/people_candidates.rb +21 -9
- data/lib/permission_forge/admin/people_directory.rb +23 -5
- data/lib/permission_forge/admin/permission_set_factory.rb +6 -15
- data/lib/permission_forge/admin/person_label.rb +105 -16
- data/lib/permission_forge/admin/prefill.rb +92 -0
- data/lib/permission_forge/admin/record_name.rb +45 -0
- data/lib/permission_forge/admin/rule_builder.rb +77 -3
- data/lib/permission_forge/admin/rule_form.rb +44 -10
- data/lib/permission_forge/admin/rule_form_input.rb +27 -11
- data/lib/permission_forge/admin/rule_notes.rb +43 -0
- data/lib/permission_forge/admin/rule_preview.rb +96 -0
- data/lib/permission_forge/admin/sensitive_columns.rb +10 -62
- data/lib/permission_forge/admin/settings_catalog.rb +64 -12
- data/lib/permission_forge/admin/state_label.rb +97 -0
- data/lib/permission_forge/admin_method_override.rb +44 -0
- data/lib/permission_forge/api/schema.rb +322 -0
- data/lib/permission_forge/api/serializers/base_serializer.rb +87 -1
- data/lib/permission_forge/api/serializers/permission_set_group_user_serializer.rb +1 -1
- data/lib/permission_forge/api/serializers/permission_set_rule_serializer.rb +1 -1
- data/lib/permission_forge/api/serializers/permission_set_serializer.rb +5 -2
- data/lib/permission_forge/api/serializers/permission_set_user_serializer.rb +1 -1
- data/lib/permission_forge/api/serializers/resource_action_serializer.rb +1 -1
- data/lib/permission_forge/api/serializers/resource_field_serializer.rb +1 -1
- data/lib/permission_forge/api/serializers/resource_serializer.rb +1 -1
- data/lib/permission_forge/api_authorizations.rb +59 -0
- data/lib/permission_forge/api_only_admin.rb +81 -0
- data/lib/permission_forge/audit/actor_codes.rb +32 -0
- data/lib/permission_forge/audit/actor_resolver.rb +1 -2
- data/lib/permission_forge/audit/boot_warnings.rb +29 -9
- data/lib/permission_forge/audit/event_catalog.rb +16 -6
- data/lib/permission_forge/audit/event_query.rb +3 -3
- data/lib/permission_forge/audit/reason_codes.rb +25 -0
- data/lib/permission_forge/audit/runtime.rb +9 -2
- data/lib/permission_forge/audit/settings_observer.rb +2 -2
- data/lib/permission_forge/audit/unmasked_without_user.rb +169 -0
- data/lib/permission_forge/audit.rb +24 -10
- data/lib/permission_forge/boot_checks.rb +180 -63
- data/lib/permission_forge/cache/base.rb +10 -0
- data/lib/permission_forge/cache/null_cache.rb +5 -0
- data/lib/permission_forge/cache/outcome.rb +119 -0
- data/lib/permission_forge/cache/rails_cache.rb +6 -0
- data/lib/permission_forge/cache/report.rb +150 -0
- data/lib/permission_forge/cache/user_keys.rb +8 -4
- data/lib/permission_forge/cache_warnings.rb +140 -0
- data/lib/permission_forge/concerns/auto_permission_scoping.rb +19 -2
- data/lib/permission_forge/concerns/has_tenant_scoping.rb +1 -1
- data/lib/permission_forge/concerns/has_uuid.rb +1 -1
- data/lib/permission_forge/concerns/keeps_copied_values.rb +48 -0
- data/lib/permission_forge/concerns/masked_attributes/alias_access.rb +12 -2
- data/lib/permission_forge/concerns/masked_attributes/virtual_readers.rb +6 -4
- data/lib/permission_forge/concerns/masked_attributes.rb +150 -34
- data/lib/permission_forge/concerns/normalizes_label.rb +78 -0
- data/lib/permission_forge/concerns/permission_cache_accessor.rb +3 -3
- data/lib/permission_forge/concerns/permission_scoped_findable.rb +121 -29
- data/lib/permission_forge/concerns/permissioned_user.rb +1 -1
- data/lib/permission_forge/concerns/records_audit_events.rb +13 -6
- data/lib/permission_forge/concerns/requires_name.rb +37 -0
- data/lib/permission_forge/concerns/validates_column_lengths.rb +24 -0
- data/lib/permission_forge/concerns/validates_dependency.rb +5 -2
- data/lib/permission_forge/concerns/validates_required_columns.rb +74 -0
- data/lib/permission_forge/conditional_sets.rb +13 -6
- data/lib/permission_forge/configuration/admin_ui_settings.rb +18 -4
- data/lib/permission_forge/configuration/audit_settings.rb +2 -1
- data/lib/permission_forge/configuration/controller_action_settings.rb +5 -4
- data/lib/permission_forge/configuration/masked_attributes_settings.rb +73 -19
- data/lib/permission_forge/configuration/presets.rb +20 -11
- data/lib/permission_forge/configuration/query_scoping_settings.rb +55 -0
- data/lib/permission_forge/configuration/strategy_values.rb +2 -1
- data/lib/permission_forge/configuration.rb +41 -18
- data/lib/permission_forge/controller_actions/managed_index.rb +8 -7
- data/lib/permission_forge/database_reach.rb +93 -0
- data/lib/permission_forge/deprecation.rb +1 -1
- data/lib/permission_forge/docs/api_samples.rb +250 -0
- data/lib/permission_forge/docs/configuration_reference.rb +172 -0
- data/lib/permission_forge/encrypted_attributes.rb +150 -0
- data/lib/permission_forge/engine.rb +46 -14
- data/lib/permission_forge/execution_state/restore.rb +74 -0
- data/lib/permission_forge/execution_state.rb +8 -1
- data/lib/permission_forge/field_level.rb +123 -0
- data/lib/permission_forge/field_names.rb +145 -0
- data/lib/permission_forge/field_variants.rb +96 -0
- data/lib/permission_forge/generated_text.rb +95 -0
- data/lib/permission_forge/inspection_filters.rb +83 -0
- data/lib/permission_forge/managed_fields.rb +66 -0
- data/lib/permission_forge/masked_models.rb +25 -19
- data/lib/permission_forge/masking_sync.rb +100 -25
- data/lib/permission_forge/model_bindings.rb +30 -19
- data/lib/permission_forge/model_lookup.rb +42 -0
- data/lib/permission_forge/operators/encrypted_alias.rb +67 -0
- data/lib/permission_forge/operators/registry.rb +6 -0
- data/lib/permission_forge/operators/rule_column.rb +242 -27
- data/lib/permission_forge/operators/rule_path.rb +179 -0
- data/lib/permission_forge/operators/rule_problem.rb +309 -0
- data/lib/permission_forge/operators/rule_sources.rb +96 -0
- data/lib/permission_forge/operators/value_casters/postgresql_types.rb +237 -0
- data/lib/permission_forge/operators/value_casters/temporal.rb +9 -20
- data/lib/permission_forge/operators/value_casters.rb +61 -13
- data/lib/permission_forge/operators/value_typing.rb +83 -7
- data/lib/permission_forge/query_scoping_warnings.rb +22 -17
- data/lib/permission_forge/rake_audit.rb +5 -4
- data/lib/permission_forge/rake_dry_run.rb +32 -0
- data/lib/permission_forge/rake_output.rb +189 -0
- data/lib/permission_forge/record_read_access.rb +51 -0
- data/lib/permission_forge/resource_chain.rb +14 -2
- data/lib/permission_forge/schema_read/ordered_types.rb +72 -0
- data/lib/permission_forge/schema_read.rb +335 -0
- data/lib/permission_forge/secret_columns.rb +49 -0
- data/lib/permission_forge/sensitive_columns.rb +114 -0
- data/lib/permission_forge/services/attribute_access_guard.rb +22 -14
- data/lib/permission_forge/services/bulk_permission_set_assignment.rb +16 -8
- data/lib/permission_forge/services/concerns/clonable.rb +9 -0
- data/lib/permission_forge/services/concerns/settles_cache.rb +58 -0
- data/lib/permission_forge/services/consent_withdrawal_handler.rb +37 -19
- data/lib/permission_forge/services/data_subject_erasure.rb +83 -45
- data/lib/permission_forge/services/expired_assignment_cleaner.rb +62 -25
- data/lib/permission_forge/services/extra_field_defaults.rb +1 -1
- data/lib/permission_forge/services/field_permission_merger.rb +44 -17
- data/lib/permission_forge/services/filters_response_attributes.rb +20 -5
- data/lib/permission_forge/services/gdpr_field_auditor/text_report.rb +33 -5
- data/lib/permission_forge/services/gdpr_field_auditor.rb +68 -48
- data/lib/permission_forge/services/host_key_guard.rb +8 -5
- data/lib/permission_forge/services/mask_value.rb +72 -11
- data/lib/permission_forge/services/permission_cache_clearer.rb +36 -15
- data/lib/permission_forge/services/permission_hash_builder.rb +48 -19
- data/lib/permission_forge/services/permission_rule_applier.rb +131 -68
- data/lib/permission_forge/services/permission_set_cloner.rb +32 -8
- data/lib/permission_forge/services/permission_set_condition_evaluator.rb +38 -36
- data/lib/permission_forge/services/permission_set_group_cloner.rb +2 -1
- data/lib/permission_forge/services/permits_attributes/result.rb +31 -0
- data/lib/permission_forge/services/permits_attributes.rb +48 -27
- data/lib/permission_forge/services/resource_field_generator/row_builder.rb +96 -0
- data/lib/permission_forge/services/resource_field_generator.rb +71 -42
- data/lib/permission_forge/services/resource_with_fields_builder.rb +2 -1
- data/lib/permission_forge/services/rls_operator_sql.rb +83 -24
- data/lib/permission_forge/services/rls_policy_generator.rb +83 -39
- data/lib/permission_forge/services/rls_read_access_sql.rb +131 -0
- data/lib/permission_forge/services/security_audit/action_check.rb +49 -5
- data/lib/permission_forge/services/security_audit/assignment_check.rb +2 -1
- data/lib/permission_forge/services/security_audit/audit_trail_check.rb +1 -3
- data/lib/permission_forge/services/security_audit/conditions_check.rb +92 -0
- data/lib/permission_forge/services/security_audit/config_check.rb +41 -18
- data/lib/permission_forge/services/security_audit/integrity_check.rb +71 -4
- data/lib/permission_forge/services/security_audit/masking_without_user.rb +66 -0
- data/lib/permission_forge/services/security_audit/personal_data_check.rb +7 -11
- data/lib/permission_forge/services/security_audit/rule_check.rb +180 -50
- data/lib/permission_forge/services/security_audit/text_report.rb +14 -2
- data/lib/permission_forge/services/security_audit/unmanaged_actions.rb +145 -0
- data/lib/permission_forge/services/security_audit/user_model_check.rb +4 -3
- data/lib/permission_forge/services/security_audit.rb +16 -4
- data/lib/permission_forge/services/stale_assignment_auditor.rb +8 -5
- data/lib/permission_forge/timestamp_fields.rb +60 -0
- data/lib/permission_forge/unique_conflict.rb +117 -0
- data/lib/permission_forge/version.rb +1 -1
- data/lib/permission_forge/virtual_fields/discovery.rb +2 -1
- data/lib/permission_forge/virtual_fields.rb +15 -5
- data/lib/permission_forge/warnings.rb +263 -0
- data/lib/permission_forge/web_request.rb +248 -0
- data/lib/permission_forge.rb +54 -0
- data/lib/tasks/permission_forge.rake +26 -6
- data/lib/tasks/permission_forge_audit_events.rake +8 -4
- data/lib/tasks/permission_forge_cache.rake +12 -12
- data/lib/tasks/permission_forge_docs.rake +48 -0
- data/lib/tasks/permission_forge_expiration.rake +11 -5
- data/lib/tasks/permission_forge_gdpr.rake +36 -20
- metadata +84 -1
|
@@ -5,7 +5,10 @@ module PermissionForge
|
|
|
5
5
|
# @api private
|
|
6
6
|
class PermissionSetsController < ApplicationController
|
|
7
7
|
include PermissionSetEditor
|
|
8
|
+
include PermissionForge::ChangeReview
|
|
8
9
|
include PermissionForge::AuditLogGate
|
|
10
|
+
include PermissionForge::CopyRefusal
|
|
11
|
+
include PermissionForge::CacheConfirmation
|
|
9
12
|
|
|
10
13
|
FILTERS = %w[all granting muting conditional off].freeze
|
|
11
14
|
TABS = %w[resources assignments conditions audit].freeze
|
|
@@ -47,7 +50,10 @@ module PermissionForge
|
|
|
47
50
|
return render(:new, status: :unprocessable_entity) if @error
|
|
48
51
|
|
|
49
52
|
set = build_set
|
|
50
|
-
redirect_to permission_forge.permission_set_path(set), notice: t('permission_forge.flash.permission_sets.created', name: set
|
|
53
|
+
redirect_to permission_forge.permission_set_path(set), notice: t('permission_forge.flash.permission_sets.created', name: pf_record_name(set))
|
|
54
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
55
|
+
@error = pf_copy_refused_message(e, @name)
|
|
56
|
+
render :new, status: :unprocessable_entity
|
|
51
57
|
end
|
|
52
58
|
|
|
53
59
|
def show
|
|
@@ -72,7 +78,7 @@ module PermissionForge
|
|
|
72
78
|
@change_set = change_set
|
|
73
79
|
if @change_set.empty?
|
|
74
80
|
return redirect_to(pf_back_or(permission_forge.permission_set_path(@permission_set)),
|
|
75
|
-
|
|
81
|
+
pf_nothing_to_save_flash(@change_set))
|
|
76
82
|
end
|
|
77
83
|
|
|
78
84
|
load_editor
|
|
@@ -81,25 +87,22 @@ module PermissionForge
|
|
|
81
87
|
end
|
|
82
88
|
|
|
83
89
|
def apply
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
90
|
+
applied = change_set
|
|
91
|
+
outcome = cache_outcome { applied.apply! }
|
|
92
|
+
redirect_to back_to_set, **pf_applied_flash(applied, outcome)
|
|
87
93
|
rescue PermissionForge::Admin::ChangeSet::Invalid => e
|
|
88
94
|
redirect_to back_to_set, alert: t('permission_forge.flash.permission_sets.save_failed', errors: e.message)
|
|
89
95
|
end
|
|
90
96
|
|
|
91
97
|
def duplicate
|
|
92
|
-
|
|
93
|
-
t('permission_forge.permission_sets.copy_name', name: @permission_set.name)
|
|
94
|
-
)
|
|
95
|
-
copy = PermissionForge::Admin::PermissionSetFactory.duplicate(@permission_set, name: name)
|
|
98
|
+
copy = PermissionForge::Admin::PermissionSetFactory.duplicate(@permission_set)
|
|
96
99
|
group = @permission_set.permission_set_group
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
redirect_to permission_forge.permission_set_path(
|
|
100
|
+
key = group && copy.permission_set_group_id.nil? ? 'duplicated_outside_group' : 'duplicated'
|
|
101
|
+
redirect_to permission_forge.permission_set_path(copy),
|
|
102
|
+
notice: t("permission_forge.flash.permission_sets.#{key}", name: pf_record_name(@permission_set),
|
|
103
|
+
group: (pf_record_name(group) if group))
|
|
104
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
105
|
+
redirect_to permission_forge.permission_set_path(@permission_set), alert: pf_copy_refused_message(e, pf_record_name(@permission_set))
|
|
103
106
|
end
|
|
104
107
|
|
|
105
108
|
def confirm_destroy
|
|
@@ -107,7 +110,7 @@ module PermissionForge
|
|
|
107
110
|
end
|
|
108
111
|
|
|
109
112
|
def destroy
|
|
110
|
-
name = @permission_set
|
|
113
|
+
name = pf_record_name(@permission_set)
|
|
111
114
|
@permission_set.destroy!
|
|
112
115
|
redirect_to permission_forge.permission_sets_path, notice: t('permission_forge.flash.permission_sets.deleted', name: name)
|
|
113
116
|
end
|
|
@@ -121,23 +124,19 @@ module PermissionForge
|
|
|
121
124
|
@permission_set.update!(is_muting: !@permission_set.is_muting)
|
|
122
125
|
key = @permission_set.is_muting ? 'switched_muting' : 'switched_granting'
|
|
123
126
|
redirect_to permission_forge.permission_set_path(@permission_set),
|
|
124
|
-
notice: t("permission_forge.flash.permission_sets.#{key}", name: @permission_set
|
|
127
|
+
notice: t("permission_forge.flash.permission_sets.#{key}", name: pf_record_name(@permission_set))
|
|
125
128
|
end
|
|
126
129
|
|
|
127
130
|
def conditions
|
|
128
131
|
@conditions_form = PermissionForge::Admin::ConditionsForm.new(params) # the re-render reads it back
|
|
129
132
|
attrs = @conditions_form.attributes
|
|
130
133
|
@condition_errors = @conditions_form.errors
|
|
131
|
-
if @condition_errors.empty? && @permission_set
|
|
134
|
+
if @condition_errors.empty? && pf_save_changed(@permission_set, attrs, &:save) # unchanged: not saved (SaveNotice)
|
|
132
135
|
return redirect_to permission_forge.permission_set_path(@permission_set, tab: 'conditions'),
|
|
133
|
-
|
|
136
|
+
**pf_save_flash(@permission_set) { t('permission_forge.flash.permission_sets.conditions_saved') }
|
|
134
137
|
end
|
|
135
138
|
|
|
136
|
-
|
|
137
|
-
@ip_text = params[:allowed_ips].to_s
|
|
138
|
-
load_editor
|
|
139
|
-
load_conditions_tab
|
|
140
|
-
render :show, status: :unprocessable_entity
|
|
139
|
+
render_conditions_refused
|
|
141
140
|
end
|
|
142
141
|
|
|
143
142
|
private
|
|
@@ -33,10 +33,14 @@ module PermissionForge
|
|
|
33
33
|
@people_count = pf_people_count(@permission_set)
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
# A blank label over a generated one, or a blank over nothing, is no
|
|
37
|
+
# change (Admin::Prefill.posted); an unchanged action is not saved, and
|
|
38
|
+
# says so (SaveNotice).
|
|
36
39
|
def update
|
|
37
|
-
@action_record
|
|
38
|
-
@action_record
|
|
39
|
-
redirect_to pf_back_or(resource_path),
|
|
40
|
+
attrs = PermissionForge::Admin::Prefill.posted_attributes(@action_record, action_params.except(:name), :label, :description)
|
|
41
|
+
pf_save_changed(@action_record, attrs, &:save!)
|
|
42
|
+
redirect_to pf_back_or(resource_path),
|
|
43
|
+
**pf_save_flash(@action_record) { t('permission_forge.flash.actions.updated', action: @action_record.display_name) }
|
|
40
44
|
end
|
|
41
45
|
|
|
42
46
|
def confirm_destroy; end
|
|
@@ -58,9 +62,9 @@ module PermissionForge
|
|
|
58
62
|
@action_record = @resource.resource_actions.find(params[:id])
|
|
59
63
|
end
|
|
60
64
|
|
|
65
|
+
# The action's chain (action, Resource, Permission Set), or the Resource's for a new action.
|
|
61
66
|
def reject_locked
|
|
62
|
-
|
|
63
|
-
pf_reject_locked!(locked, resource_path) if locked
|
|
67
|
+
pf_reject_locked!(@action_record || @resource, resource_path)
|
|
64
68
|
end
|
|
65
69
|
|
|
66
70
|
def resource_path
|
|
@@ -68,7 +72,8 @@ module PermissionForge
|
|
|
68
72
|
end
|
|
69
73
|
|
|
70
74
|
def action_params
|
|
71
|
-
pf_require_hash(:resource_action).permit(:name, :label, :description, :is_permitted, :is_active)
|
|
75
|
+
pf_boolean_attributes(pf_require_hash(:resource_action).permit(:name, :label, :description, :is_permitted, :is_active),
|
|
76
|
+
:is_permitted, :is_active)
|
|
72
77
|
end
|
|
73
78
|
|
|
74
79
|
# "Export CSV " => "export_csv"
|
|
@@ -5,6 +5,8 @@ module PermissionForge
|
|
|
5
5
|
# @api private
|
|
6
6
|
class ResourceFieldsController < ApplicationController
|
|
7
7
|
include PermissionForge::ActingAdmin
|
|
8
|
+
include PermissionForge::CacheConfirmation
|
|
9
|
+
include PermissionForge::ChangeReview
|
|
8
10
|
|
|
9
11
|
before_action :load_field
|
|
10
12
|
before_action :reject_locked, only: %i[update acknowledge apply_recommendation]
|
|
@@ -21,10 +23,14 @@ module PermissionForge
|
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
def update
|
|
24
|
-
attrs = pf_require_hash(:field).permit(:level, :mask_strategy, :label, :description, :is_active).to_h
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
attrs = pf_boolean_attributes(pf_require_hash(:field).permit(:level, :mask_strategy, :label, :description, :is_active).to_h,
|
|
27
|
+
:is_active)
|
|
28
|
+
applied = nil
|
|
29
|
+
outcome = cache_outcome do
|
|
30
|
+
applied = apply_field_changes(attrs)
|
|
31
|
+
finish_new_field(attrs) unless @field
|
|
32
|
+
end
|
|
33
|
+
redirect_to pf_back_or(resource_path), **pf_applied_flash(applied, outcome)
|
|
28
34
|
rescue PermissionForge::Admin::ChangeSet::Invalid => e
|
|
29
35
|
redirect_to field_path, alert: t('permission_forge.flash.permission_sets.save_failed', errors: e.message)
|
|
30
36
|
end
|
|
@@ -43,12 +49,13 @@ module PermissionForge
|
|
|
43
49
|
|
|
44
50
|
# Applies the recommended mask for a sensitive column.
|
|
45
51
|
def apply_recommendation
|
|
46
|
-
match = PermissionForge::
|
|
52
|
+
match = PermissionForge::SensitiveColumns.new.match(@key)
|
|
47
53
|
return redirect_to(field_path) unless match
|
|
48
54
|
|
|
49
55
|
level = @permission_set.is_muting ? 'mask' : 'masked'
|
|
50
|
-
apply_field_changes('level' => level, 'mask_strategy' => match.strategy)
|
|
51
|
-
redirect_to pf_back_or(field_path),
|
|
56
|
+
applied = apply_field_changes('level' => level, 'mask_strategy' => match.strategy)
|
|
57
|
+
redirect_to pf_back_or(field_path),
|
|
58
|
+
**pf_save_flash(applied.changes.any?) { t('permission_forge.flash.fields.recommendation_applied', field: @key) }
|
|
52
59
|
end
|
|
53
60
|
|
|
54
61
|
private
|
|
@@ -68,13 +75,15 @@ module PermissionForge
|
|
|
68
75
|
|
|
69
76
|
# Applies `attrs` to this field through a ChangeSet; an unconfigured column
|
|
70
77
|
# is created at attrs['level'].
|
|
78
|
+
#
|
|
79
|
+
# @return [PermissionForge::Admin::ChangeSet] the applied change set
|
|
71
80
|
def apply_field_changes(attrs)
|
|
72
81
|
change_params = if @field
|
|
73
82
|
{ fields: { @field.id.to_s => attrs } }
|
|
74
83
|
else
|
|
75
84
|
{ new_fields: { @resource.id.to_s => { @key => { 'level' => attrs['level'] } } } }
|
|
76
85
|
end
|
|
77
|
-
PermissionForge::Admin::ChangeSet.new(permission_set: @permission_set, params: change_params).apply!
|
|
86
|
+
PermissionForge::Admin::ChangeSet.new(permission_set: @permission_set, params: change_params).tap(&:apply!)
|
|
78
87
|
end
|
|
79
88
|
|
|
80
89
|
# Filter rules (no relationship) on this field.
|
|
@@ -82,9 +91,9 @@ module PermissionForge
|
|
|
82
91
|
@resource.permission_set_rules.select { |rule| rule.field_name == @key && rule.field_accessor_path.blank? }
|
|
83
92
|
end
|
|
84
93
|
|
|
94
|
+
# The field's chain (field, Resource, Permission Set), or the Resource's for a column with no row.
|
|
85
95
|
def reject_locked
|
|
86
|
-
|
|
87
|
-
pf_reject_locked!(locked, field_path) if locked
|
|
96
|
+
pf_reject_locked!(@field || @resource, field_path)
|
|
88
97
|
end
|
|
89
98
|
|
|
90
99
|
def resource_path
|
|
@@ -104,15 +113,10 @@ module PermissionForge
|
|
|
104
113
|
field.update!(extra) if extra.any?
|
|
105
114
|
end
|
|
106
115
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
t('permission_forge.flash.permission_sets.saved', count: changes.size,
|
|
111
|
-
people: helpers.pf_people_text(pf_people_count(@permission_set)))
|
|
112
|
-
end
|
|
113
|
-
|
|
116
|
+
# People holding a set with this Resource, or one of the STI bases its
|
|
117
|
+
# records answer to (ResourceChain: a base's rows reach the subclass).
|
|
114
118
|
def explain_candidates
|
|
115
|
-
set_ids = PermissionForge::Resource.where(name:
|
|
119
|
+
set_ids = PermissionForge::Resource.where(name: chain_names).select(:permission_set_id)
|
|
116
120
|
user_ids = PermissionForge::PermissionSetUser.active_assignments.where(permission_set_id: set_ids)
|
|
117
121
|
.distinct.limit(200).pluck(:user_id)
|
|
118
122
|
PermissionForge::Admin::PeopleDirectory.new.find_all(user_ids)
|
|
@@ -122,8 +126,22 @@ module PermissionForge
|
|
|
122
126
|
user = PermissionForge::Admin::PeopleDirectory.new.find(user_id)
|
|
123
127
|
return nil unless user
|
|
124
128
|
|
|
125
|
-
outcome = PermissionForge::Admin::EffectiveAccess.call(user: user)
|
|
129
|
+
outcome = nearest_outcome(PermissionForge::Admin::EffectiveAccess.call(user: user))
|
|
126
130
|
{ user: user, field: outcome&.fields&.find { |field| field.name == @key } }
|
|
127
131
|
end
|
|
132
|
+
|
|
133
|
+
def chain_names
|
|
134
|
+
PermissionForge::ResourceChain.names_for(@resource.name).map(&:to_s)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# The person's outcome for this model: its own entry, else the nearest
|
|
138
|
+
# base's (a subclass without an entry of its own is its base).
|
|
139
|
+
def nearest_outcome(outcomes)
|
|
140
|
+
chain_names.each do |name|
|
|
141
|
+
outcome = outcomes.find { |r| r.name == name }
|
|
142
|
+
return outcome if outcome
|
|
143
|
+
end
|
|
144
|
+
nil
|
|
145
|
+
end
|
|
128
146
|
end
|
|
129
147
|
end
|
|
@@ -4,6 +4,9 @@ module PermissionForge
|
|
|
4
4
|
#
|
|
5
5
|
# @api private
|
|
6
6
|
class ResourcesController < ApplicationController
|
|
7
|
+
include PermissionForge::ChangeReview
|
|
8
|
+
include PermissionForge::CacheConfirmation
|
|
9
|
+
|
|
7
10
|
TABS = %w[fields actions rules].freeze
|
|
8
11
|
GRANTING_PRESETS = %w[full read nothing].freeze
|
|
9
12
|
MUTING_PRESETS = %w[hide_sensitive block_changes leave].freeze
|
|
@@ -15,7 +18,6 @@ module PermissionForge
|
|
|
15
18
|
}.freeze
|
|
16
19
|
# Field level each preset gives the model's columns (nil = no fields).
|
|
17
20
|
PRESET_LEVELS = { 'full' => 'edit', 'read' => 'read', 'nothing' => 'hidden', 'hide_sensitive' => 'hide_sensitive' }.freeze
|
|
18
|
-
|
|
19
21
|
before_action :set_permission_set
|
|
20
22
|
before_action :set_resource, except: %i[new create]
|
|
21
23
|
before_action :reject_locked_set, only: %i[new create review apply sync destroy confirm_destroy]
|
|
@@ -42,7 +44,7 @@ module PermissionForge
|
|
|
42
44
|
|
|
43
45
|
def review
|
|
44
46
|
@change_set = change_set
|
|
45
|
-
return redirect_to(resource_path,
|
|
47
|
+
return redirect_to(resource_path, pf_nothing_to_save_flash(@change_set)) if @change_set.empty?
|
|
46
48
|
|
|
47
49
|
load_page
|
|
48
50
|
@changes = @change_set.changes
|
|
@@ -50,10 +52,9 @@ module PermissionForge
|
|
|
50
52
|
end
|
|
51
53
|
|
|
52
54
|
def apply
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
people: helpers.pf_people_text(pf_people_count(@permission_set)))
|
|
55
|
+
applied = change_set
|
|
56
|
+
outcome = cache_outcome { applied.apply! }
|
|
57
|
+
redirect_to pf_back_or(resource_path), **pf_applied_flash(applied, outcome)
|
|
57
58
|
rescue PermissionForge::Admin::ChangeSet::Invalid => e
|
|
58
59
|
redirect_to resource_path, alert: t('permission_forge.flash.permission_sets.save_failed', errors: e.message)
|
|
59
60
|
end
|
|
@@ -77,7 +78,7 @@ module PermissionForge
|
|
|
77
78
|
label = helpers.pf_resource_label(@resource)
|
|
78
79
|
@resource.destroy!
|
|
79
80
|
redirect_to permission_forge.permission_set_path(@permission_set),
|
|
80
|
-
notice: t('permission_forge.flash.resources.removed', resource: label, set: @permission_set
|
|
81
|
+
notice: t('permission_forge.flash.resources.removed', resource: label, set: pf_record_name(@permission_set))
|
|
81
82
|
end
|
|
82
83
|
|
|
83
84
|
private
|
|
@@ -138,7 +139,7 @@ module PermissionForge
|
|
|
138
139
|
|
|
139
140
|
def load_new_form
|
|
140
141
|
load_models
|
|
141
|
-
@sensitive = PermissionForge::
|
|
142
|
+
@sensitive = PermissionForge::SensitiveColumns.new
|
|
142
143
|
@existing = @permission_set.resources.pluck(:name)
|
|
143
144
|
@presets = @permission_set.is_muting ? MUTING_PRESETS : GRANTING_PRESETS
|
|
144
145
|
preset = params[:preset]
|
|
@@ -161,8 +162,7 @@ module PermissionForge
|
|
|
161
162
|
end
|
|
162
163
|
|
|
163
164
|
def added_notice(resource)
|
|
164
|
-
|
|
165
|
-
count: resource.resource_fields.size)
|
|
165
|
+
helpers.pf_added_resource_notice(resource, muting: @permission_set.is_muting)
|
|
166
166
|
end
|
|
167
167
|
|
|
168
168
|
def build_resource(model, preset)
|
|
@@ -181,7 +181,8 @@ module PermissionForge
|
|
|
181
181
|
end
|
|
182
182
|
|
|
183
183
|
# Creates ResourceFields for columns and virtual fields at a level, applying
|
|
184
|
-
# privacy defaults (
|
|
184
|
+
# privacy defaults (see Admin::ColumnFields: under gdpr_defaults sensitive
|
|
185
|
+
# names start Masked, except at full access).
|
|
185
186
|
def create_fields(columns, level, resource: @resource)
|
|
186
187
|
PermissionForge::Admin::ColumnFields.new(resource, muting: @permission_set.is_muting).create(columns, level)
|
|
187
188
|
end
|
|
@@ -14,9 +14,11 @@
|
|
|
14
14
|
--pf-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
15
15
|
|
|
16
16
|
--pf-radius-xs: 4px;
|
|
17
|
+
--pf-scroll-shadow: rgba(16, 18, 22, .16);
|
|
17
18
|
--pf-radius-sm: 6px;
|
|
18
19
|
--pf-radius: 8px;
|
|
19
20
|
--pf-radius-lg: 12px;
|
|
21
|
+
--pf-sidebar-width: 248px;
|
|
20
22
|
|
|
21
23
|
--pf-space-1: 4px;
|
|
22
24
|
--pf-space-2: 8px;
|
|
@@ -70,6 +72,7 @@
|
|
|
70
72
|
--pf-bg: #111214;
|
|
71
73
|
--pf-surface: #18191c;
|
|
72
74
|
--pf-surface-2: #212327;
|
|
75
|
+
--pf-scroll-shadow: rgba(0, 0, 0, .55);
|
|
73
76
|
--pf-border: #2c2e33;
|
|
74
77
|
--pf-border-strong: #3c3f45;
|
|
75
78
|
--pf-text: #ececee;
|
|
@@ -105,6 +108,7 @@
|
|
|
105
108
|
--pf-bg: #111214;
|
|
106
109
|
--pf-surface: #18191c;
|
|
107
110
|
--pf-surface-2: #212327;
|
|
111
|
+
--pf-scroll-shadow: rgba(0, 0, 0, .55);
|
|
108
112
|
--pf-border: #2c2e33;
|
|
109
113
|
--pf-border-strong: #3c3f45;
|
|
110
114
|
--pf-text: #ececee;
|
|
@@ -212,7 +216,7 @@
|
|
|
212
216
|
|
|
213
217
|
/* ------------------------------------------------------------------ layout */
|
|
214
218
|
.pf-app { min-height: 100vh; display: flex; flex-wrap: wrap; align-content: flex-start; background: var(--pf-bg); }
|
|
215
|
-
.pf-sidebar { flex: 0 0
|
|
219
|
+
.pf-sidebar { flex: 0 0 var(--pf-sidebar-width); align-self: stretch; border-right: 1px solid var(--pf-border); background: var(--pf-surface); }
|
|
216
220
|
.pf-sidebar__inner { position: sticky; top: 0; height: 100vh; overflow: auto; display: flex; flex-direction: column; gap: 2px; padding: 20px 12px; }
|
|
217
221
|
.pf-brand { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; padding: 4px 8px 18px; }
|
|
218
222
|
.pf-brand__home { display: inline-block; }
|
|
@@ -306,11 +310,17 @@
|
|
|
306
310
|
.pf-root .pf-input:focus, .pf-root .pf-textarea:focus { outline: none; border-color: var(--pf-accent); box-shadow: 0 0 0 3px var(--pf-accent-soft); }
|
|
307
311
|
.pf-input--sm, .pf-select--sm { padding: 6px 10px; font-size: 13px; }
|
|
308
312
|
.pf-input--lg { padding: 10px 12px; font-size: 15px; }
|
|
309
|
-
.pf-input--auto, .pf-select--auto { display: inline-block; width: auto; max-width: 100%; }
|
|
313
|
+
.pf-input--auto, .pf-select--auto { display: inline-block; width: auto; min-width: 0; max-width: 100%; }
|
|
310
314
|
.pf-textarea { min-height: 72px; resize: vertical; }
|
|
311
315
|
.pf-textarea--mono { font-family: var(--pf-mono); font-size: 13px; max-width: 360px; }
|
|
312
316
|
.pf-input[aria-invalid="true"], .pf-select[aria-invalid="true"], .pf-textarea[aria-invalid="true"] { border-color: var(--pf-block); }
|
|
313
317
|
.pf-input::placeholder, .pf-textarea::placeholder { color: var(--pf-text-3); }
|
|
318
|
+
/* Read-only (a locked record): a filled box that does not light up on hover, so it
|
|
319
|
+
does not read as editable. It stays focusable (announced "read only") with a
|
|
320
|
+
visible, neutral focus ring. */
|
|
321
|
+
.pf-root .pf-input[readonly], .pf-root .pf-textarea[readonly] { background: var(--pf-surface-2); border-color: var(--pf-border); color: var(--pf-text-2); cursor: default; resize: none; }
|
|
322
|
+
.pf-root .pf-input[readonly]:hover, .pf-root .pf-textarea[readonly]:hover { border-color: var(--pf-border); }
|
|
323
|
+
.pf-root .pf-input[readonly]:focus, .pf-root .pf-textarea[readonly]:focus { border-color: var(--pf-border-strong); box-shadow: 0 0 0 3px var(--pf-border); }
|
|
314
324
|
.pf-label { display: block; font-size: 13px; font-weight: 600; color: var(--pf-text); margin-bottom: 6px; }
|
|
315
325
|
.pf-form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr)); gap: 12px 16px; align-items: end; }
|
|
316
326
|
.pf-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
|
|
@@ -322,9 +332,19 @@
|
|
|
322
332
|
.pf-title-input { width: 100%; font-size: 24px; line-height: 1.25; font-weight: 600; letter-spacing: -0.01em; border: 1px solid transparent; border-radius: var(--pf-radius-sm); background: transparent; margin-left: -8px; padding: 4px 8px; }
|
|
323
333
|
.pf-title-input:hover { border-color: var(--pf-border); }
|
|
324
334
|
.pf-title-input:focus { border-color: var(--pf-border-strong); background: var(--pf-surface); }
|
|
325
|
-
.pf-desc-input { width: 100%; font-size: 14px; color: var(--pf-text-2); border: 1px solid transparent; border-radius: var(--pf-radius-sm); background: transparent; margin-left: -8px; padding: 3px 8px; }
|
|
335
|
+
.pf-desc-input { width: 100%; font-size: 14px; color: var(--pf-text-2); border: 1px solid transparent; border-radius: var(--pf-radius-sm); background: transparent; margin-left: -8px; padding: 3px 8px; text-overflow: ellipsis; }
|
|
326
336
|
.pf-desc-input:hover { border-color: var(--pf-border); }
|
|
327
337
|
.pf-desc-input:focus { border-color: var(--pf-border-strong); background: var(--pf-surface); color: var(--pf-text); }
|
|
338
|
+
/* A locked record's name and description: plain text that does not invite an edit. */
|
|
339
|
+
/* The title shows "Untitled …" as its placeholder only while the name is blank; on a phone the
|
|
340
|
+
title is smaller, and a name or placeholder longer than the box ends in an ellipsis. */
|
|
341
|
+
.pf-title-input { text-overflow: ellipsis; }
|
|
342
|
+
@media (max-width: 640px) { .pf-title-input { font-size: 20px; } }
|
|
343
|
+
.pf-title-input[readonly], .pf-desc-input[readonly] { cursor: default; }
|
|
344
|
+
.pf-title-input[readonly]:hover, .pf-desc-input[readonly]:hover { border-color: transparent; }
|
|
345
|
+
.pf-title-input[readonly]:focus, .pf-desc-input[readonly]:focus { background: transparent; }
|
|
346
|
+
.pf-desc-input[readonly]:focus { color: var(--pf-text-2); }
|
|
347
|
+
.pf-desc-input[readonly]::placeholder { color: transparent; }
|
|
328
348
|
|
|
329
349
|
/* switch: <label class="pf-switch"><input type="checkbox" role="switch"><span class="pf-switch__track"></span>Text</label> */
|
|
330
350
|
.pf-switch { display: inline-flex; align-items: center; gap: 8px; padding: 6px 12px; border: 1px solid var(--pf-border-strong); border-radius: var(--pf-radius-sm); background: var(--pf-surface); font-size: 13px; font-weight: 500; cursor: pointer; white-space: nowrap; }
|
|
@@ -363,8 +383,19 @@
|
|
|
363
383
|
.pf-masonry__col { flex: 1 1 min(100%, 460px); min-width: 0; display: flex; flex-direction: column; gap: 16px; }
|
|
364
384
|
.pf-grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr)); gap: 16px; align-items: start; }
|
|
365
385
|
.pf-grid-3 { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 250px), 1fr)); gap: 12px; align-items: start; }
|
|
366
|
-
|
|
367
|
-
.
|
|
386
|
+
/* A frame that scrolls sideways when a grid is wider than the page (pf_scroll_frame: a tab
|
|
387
|
+
stop with a name). It is the containing block of what it clips (position: relative): an
|
|
388
|
+
absolutely positioned descendant (a .pf-sr-only header cell) would otherwise sit at its
|
|
389
|
+
static position in the page, outside the frame's clipping, and widen the document instead
|
|
390
|
+
of scrolling with the grid. The shadows at its edges are the cue that there is more: covers
|
|
391
|
+
that move with the content (`local`) hide them on a side with nothing left to scroll to.
|
|
392
|
+
Every scroll container is positioned for the same reason (layout_lint_test.rb). */
|
|
393
|
+
.pf-scroll-x { position: relative; overflow-x: auto; background-color: var(--pf-surface);
|
|
394
|
+
background-image: linear-gradient(90deg, var(--pf-surface) 50%, transparent), linear-gradient(270deg, var(--pf-surface) 50%, transparent),
|
|
395
|
+
linear-gradient(90deg, var(--pf-scroll-shadow), transparent), linear-gradient(270deg, var(--pf-scroll-shadow), transparent);
|
|
396
|
+
background-position: left center, right center, left center, right center; background-repeat: no-repeat;
|
|
397
|
+
background-size: 32px 100%, 32px 100%, 12px 100%, 12px 100%; background-attachment: local, local, scroll, scroll; }
|
|
398
|
+
.pf-scroll-y { position: relative; max-height: 260px; overflow-y: auto; }
|
|
368
399
|
.pf-count-pill { display: inline-flex; align-items: center; min-width: 22px; padding: 0 8px; border-radius: 999px; font-size: 12px; font-weight: 600; line-height: 20px; justify-content: center; background: var(--pf-surface-2); color: var(--pf-text-2); border: 1px solid var(--pf-border); }
|
|
369
400
|
.pf-count-pill--warn { background: var(--pf-warn-soft); color: var(--pf-warn); border-color: var(--pf-warn-border); }
|
|
370
401
|
|
|
@@ -425,15 +456,44 @@
|
|
|
425
456
|
.pf-table--sets { min-width: 560px; }
|
|
426
457
|
.pf-table--sets .pf-table__head, .pf-table--sets .pf-table__row { grid-template-columns: minmax(0, 2fr) 56px 76px minmax(0, .8fr) auto; }
|
|
427
458
|
.pf-table--assignments { min-width: 640px; }
|
|
428
|
-
.pf-table--assignments .pf-table__head, .pf-table--assignments .pf-table__row { grid-template-columns:
|
|
459
|
+
.pf-table--assignments .pf-table__head, .pf-table--assignments .pf-table__row { grid-template-columns: auto minmax(0, 1.5fr) minmax(0, 1.2fr) minmax(0, 1fr) auto; }
|
|
429
460
|
.pf-table--audit { min-width: 640px; }
|
|
430
461
|
.pf-table--audit .pf-table__head, .pf-table--audit .pf-table__row { grid-template-columns: 110px minmax(0, 1.6fr) minmax(0, 1fr) minmax(0, 1fr); }
|
|
462
|
+
/* A table with an `auto` column (sized by its content) shares one grid between
|
|
463
|
+
its head and rows: each row on its own grid would size that column to its own
|
|
464
|
+
content, and the headers would drift off their cells. The edge tracks are
|
|
465
|
+
`auto` or `fr` (never a fixed width): a subgrid adds its padding to them. */
|
|
466
|
+
@supports (grid-template-columns: subgrid) {
|
|
467
|
+
.pf-table--sets, .pf-table--assignments { display: grid; column-gap: 12px; }
|
|
468
|
+
.pf-table--sets { grid-template-columns: minmax(0, 2fr) 56px 76px minmax(0, .8fr) auto; }
|
|
469
|
+
.pf-table--assignments { grid-template-columns: auto minmax(0, 1.5fr) minmax(0, 1.2fr) minmax(0, 1fr) fit-content(40%); }
|
|
470
|
+
.pf-table--sets > .pf-table__head, .pf-table--sets > .pf-table__row,
|
|
471
|
+
.pf-table--assignments > .pf-table__head, .pf-table--assignments > .pf-table__row { grid-column: 1 / -1; grid-template-columns: subgrid; }
|
|
472
|
+
}
|
|
473
|
+
/* A narrow card (a phone) stacks each Assignments row instead of scrolling it
|
|
474
|
+
sideways: the checkbox, then the person, the dates and the actions one under
|
|
475
|
+
another, so the end-date menu, Reason and Remove wrap inside the row. The
|
|
476
|
+
card's width decides, not the window's (the sidebar takes room at desktop
|
|
477
|
+
widths); without container queries the card still scrolls. The head keeps
|
|
478
|
+
only its first two columns on screen (the rest stay for screen readers). */
|
|
479
|
+
.pf-table-frame { container-type: inline-size; }
|
|
480
|
+
@container (max-width: 639px) {
|
|
481
|
+
.pf-table-frame .pf-table--assignments { display: block; min-width: 0; }
|
|
482
|
+
.pf-table-frame .pf-table--assignments > .pf-table__head,
|
|
483
|
+
.pf-table-frame .pf-table--assignments > .pf-table__row { grid-template-columns: auto minmax(0, 1fr); padding: 10px 16px; }
|
|
484
|
+
.pf-table-frame .pf-table--assignments > * > :first-child { grid-column: 1; grid-row: 1; }
|
|
485
|
+
.pf-table-frame .pf-table--assignments > * > :not(:first-child) { grid-column: 2; }
|
|
486
|
+
.pf-table-frame .pf-table--assignments > .pf-table__head > :nth-child(n+3) {
|
|
487
|
+
position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap;
|
|
488
|
+
}
|
|
489
|
+
.pf-table-frame .pf-table--assignments .pf-reason-form { max-width: 100%; }
|
|
490
|
+
}
|
|
431
491
|
.pf-cell-person { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
432
492
|
.pf-avatar { display: inline-grid; place-items: center; width: 30px; height: 30px; flex: 0 0 30px; border-radius: 999px; background: var(--pf-surface-2); border: 1px solid var(--pf-border); font-size: 11px; font-weight: 600; color: var(--pf-text-2); }
|
|
433
493
|
.pf-avatar--sm { width: 28px; height: 28px; flex-basis: 28px; }
|
|
434
494
|
.pf-avatar--lg { width: 48px; height: 48px; flex-basis: 48px; font-size: 16px; }
|
|
435
495
|
.pf-person-name { font-weight: 600; }
|
|
436
|
-
.pf-person-email { font-size: 12px; color: var(--pf-text-3); }
|
|
496
|
+
.pf-person-email { font-size: 12px; color: var(--pf-text-3); overflow-wrap: anywhere; }
|
|
437
497
|
|
|
438
498
|
/* ------------------------------------------------------------- matrices */
|
|
439
499
|
.pf-matrix { min-width: 620px; }
|
|
@@ -441,7 +501,7 @@
|
|
|
441
501
|
.pf-matrix__head { font-size: 12px; font-weight: 600; color: var(--pf-text-3); border-bottom: 1px solid var(--pf-border); }
|
|
442
502
|
.pf-matrix__row { border-top: 1px solid var(--pf-border); }
|
|
443
503
|
.pf-matrix__head + .pf-matrix__row { border-top: 0; }
|
|
444
|
-
.pf-matrix__resource { display: flex; flex-direction: column; min-width: 0; }
|
|
504
|
+
.pf-matrix__resource { display: flex; flex-direction: column; min-width: 0; overflow-wrap: anywhere; }
|
|
445
505
|
.pf-matrix__resource a { font-weight: 600; color: var(--pf-text) !important; }
|
|
446
506
|
.pf-matrix__chev { color: var(--pf-text-3) !important; font-size: 16px; text-align: center; text-decoration: none !important; }
|
|
447
507
|
.pf-effective { min-width: 540px; }
|
|
@@ -485,6 +545,7 @@
|
|
|
485
545
|
.pf-state[aria-current="true"], .pf-effective__field[aria-current="true"], .pf-action-pill[aria-current="true"] { box-shadow: 0 0 0 2px var(--pf-accent); }
|
|
486
546
|
.pf-state .pf-cell__glyph { font-weight: 700; }
|
|
487
547
|
.pf-state--records { flex-direction: column; align-items: flex-start; justify-content: center; gap: 0; min-width: 0; width: 100%; overflow: hidden; text-align: left; }
|
|
548
|
+
.pf-state__summary { overflow-wrap: anywhere; }
|
|
488
549
|
|
|
489
550
|
/* 4-level field control */
|
|
490
551
|
.pf-levels { display: inline-flex; flex-wrap: wrap; gap: 2px; padding: 2px; border-radius: var(--pf-radius-sm); background: var(--pf-surface-2); border: 1px solid var(--pf-border); }
|
|
@@ -515,6 +576,10 @@
|
|
|
515
576
|
.pf-root a.pf-fields__name:hover .pf-fields__label { text-decoration: underline; }
|
|
516
577
|
.pf-fields__label { font-weight: 500; }
|
|
517
578
|
.pf-mask-preview { font-size: 12px; color: var(--pf-mask); font-family: var(--pf-mono); white-space: nowrap; }
|
|
579
|
+
/* The level control and, under it, the mask preview: the control alone sets the
|
|
580
|
+
width (the preview's own width does not count), so controls line up across rows. */
|
|
581
|
+
.pf-fields__level { display: flex; flex-direction: column; align-items: stretch; gap: 2px; max-width: 100%; }
|
|
582
|
+
.pf-fields__level > .pf-mask-preview { contain: inline-size; overflow: hidden; text-overflow: ellipsis; text-align: right; }
|
|
518
583
|
.pf-bulk-bar { position: sticky; top: 8px; z-index: 5; display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; padding: 10px 14px; border-radius: var(--pf-radius); background: var(--pf-text); color: var(--pf-surface); box-shadow: var(--pf-shadow); }
|
|
519
584
|
.pf-bulk-bar .pf-btn { background: transparent; color: var(--pf-surface) !important; border-color: rgba(127, 127, 127, .55); }
|
|
520
585
|
.pf-bulk-bar .pf-btn:hover { background: rgba(127, 127, 127, .25); }
|
|
@@ -562,7 +627,7 @@
|
|
|
562
627
|
|
|
563
628
|
/* person picker / listbox rows */
|
|
564
629
|
.pf-listbox { display: flex; flex-direction: column; border: 1px solid var(--pf-border); border-radius: var(--pf-radius); overflow: hidden; }
|
|
565
|
-
.pf-listbox__scroll { max-height: 300px; overflow-y: auto; }
|
|
630
|
+
.pf-listbox__scroll { position: relative; max-height: 300px; overflow-y: auto; }
|
|
566
631
|
.pf-listbox--short .pf-listbox__scroll { max-height: 240px; }
|
|
567
632
|
.pf-option { position: relative; display: flex; align-items: center; gap: 10px; padding: 8px 12px; border-top: 1px solid var(--pf-border); color: inherit !important; text-decoration: none !important; cursor: pointer; }
|
|
568
633
|
.pf-option:first-child { border-top: 0; }
|
|
@@ -572,11 +637,14 @@
|
|
|
572
637
|
.pf-option:has(input:disabled) { opacity: .6; cursor: not-allowed; }
|
|
573
638
|
.pf-option__main { display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto; }
|
|
574
639
|
.pf-option__aside { font-size: 12px; color: var(--pf-text-3); white-space: nowrap; }
|
|
640
|
+
/* On a phone a row's badge and count go to a second line rather than past the edge. */
|
|
641
|
+
@media (max-width: 640px) { .pf-option { flex-wrap: wrap; row-gap: 4px; } }
|
|
575
642
|
.pf-listbox__footer { padding: 8px 12px; background: var(--pf-surface-2); border-top: 1px solid var(--pf-border); font-size: 12px; color: var(--pf-text-2); }
|
|
576
643
|
.pf-picked { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border: 1px solid var(--pf-border); border-radius: var(--pf-radius); background: var(--pf-surface-2); }
|
|
577
644
|
|
|
578
645
|
/* home search toolbar */
|
|
579
646
|
.pf-toolbar { display: flex; flex-wrap: wrap; align-items: stretch; gap: 8px; }
|
|
647
|
+
.pf-toolbar__help { flex: 1 1 100%; }
|
|
580
648
|
.pf-search-field { position: relative; flex: 1 1 320px; min-width: 0; }
|
|
581
649
|
.pf-search-field__icon { position: absolute; left: 12px; top: 50%; width: 16px; height: 16px; transform: translateY(-50%); color: var(--pf-text-3); pointer-events: none; }
|
|
582
650
|
.pf-root .pf-search-field__input { padding: 9px 12px 9px 36px; font-size: 14px; }
|
|
@@ -616,21 +684,23 @@
|
|
|
616
684
|
/* ------------------------------------------------------------- settings */
|
|
617
685
|
.pf-setting { display: flex; flex-wrap: wrap; gap: 10px 20px; padding: 12px 16px; border-top: 1px solid var(--pf-border); scroll-margin-top: 80px; }
|
|
618
686
|
.pf-setting:target, .pf-setting[data-pf-focus="true"] { background: var(--pf-accent-soft); }
|
|
619
|
-
.pf-setting__about { flex: 2 1 300px; display: flex; flex-direction: column; gap: 4px; min-width: 0; }
|
|
620
|
-
.pf-setting__value { flex: 1 1 240px; display: flex; flex-direction: column; align-items: flex-start; gap: 6px; min-width: 0; }
|
|
687
|
+
.pf-setting__about { flex: 2 1 300px; display: flex; flex-direction: column; gap: 4px; min-width: 0; overflow-wrap: anywhere; }
|
|
688
|
+
.pf-setting__value { flex: 1 1 240px; display: flex; flex-direction: column; align-items: flex-start; gap: 6px; min-width: 0; overflow-wrap: anywhere; }
|
|
689
|
+
/* A value's pill wraps inside its column (a long translation must not widen the page). */
|
|
690
|
+
.pf-setting__value .pf-pill { white-space: normal; max-width: 100%; }
|
|
621
691
|
.pf-setting__how { flex: 0 0 auto; display: flex; flex-direction: column; align-items: flex-end; gap: 10px; }
|
|
622
|
-
.pf-setting__how[open] { flex: 1 1 100%; }
|
|
692
|
+
.pf-setting__how[open] { flex: 1 1 100%; min-width: 0; }
|
|
623
693
|
.pf-setting__how > summary { list-style: none; }
|
|
624
694
|
.pf-setting__how > summary::-webkit-details-marker { display: none; }
|
|
625
|
-
.pf-setting__panel { flex: 1 1 100%; display: flex; flex-direction: column; gap: 8px; padding: 12px 14px; border-radius: var(--pf-radius); background: var(--pf-surface-2); border: 1px solid var(--pf-border); }
|
|
695
|
+
.pf-setting__panel { flex: 1 1 100%; align-self: stretch; min-width: 0; display: flex; flex-direction: column; gap: 8px; padding: 12px 14px; border-radius: var(--pf-radius); background: var(--pf-surface-2); border: 1px solid var(--pf-border); }
|
|
626
696
|
.pf-setting-section > summary { list-style: none; display: flex; align-items: center; gap: 10px; padding: 14px 16px; cursor: pointer; }
|
|
627
697
|
.pf-setting-section > summary::-webkit-details-marker { display: none; }
|
|
628
698
|
.pf-setting-section + .pf-setting-section { border-top: 1px solid var(--pf-border); }
|
|
629
699
|
.pf-pill { display: inline-flex; align-items: center; gap: 4px; padding: 1px 8px; border-radius: 999px; font-size: 12px; font-weight: 600; border: 1px solid var(--pf-border); background: var(--pf-surface-2); color: var(--pf-text-2); white-space: nowrap; }
|
|
630
700
|
.pf-pill--on { background: var(--pf-grant-soft); color: var(--pf-grant); border-color: var(--pf-grant-border); }
|
|
631
|
-
.pf-pill--opt { font-family: var(--pf-mono); font-weight: 500; background: transparent; }
|
|
701
|
+
.pf-pill--opt { font-family: var(--pf-mono); font-weight: 500; background: transparent; white-space: normal; overflow-wrap: anywhere; }
|
|
632
702
|
.pf-pill--current { background: var(--pf-text); color: var(--pf-surface); border-color: var(--pf-text); }
|
|
633
|
-
.pf-code { margin: 0; padding: 12px 14px; border-radius: var(--pf-radius); background: var(--pf-surface); border: 1px solid var(--pf-border); font-family: var(--pf-mono); font-size: 12.5px; line-height: 1.6; overflow-x: auto; white-space: pre; }
|
|
703
|
+
.pf-code { position: relative; margin: 0; padding: 12px 14px; border-radius: var(--pf-radius); background: var(--pf-surface); border: 1px solid var(--pf-border); font-family: var(--pf-mono); font-size: 12.5px; line-height: 1.6; overflow-x: auto; white-space: pre; }
|
|
634
704
|
.pf-ladder { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr)); gap: 10px; padding: 0 16px 14px; }
|
|
635
705
|
.pf-ladder__tile { display: flex; flex-direction: column; gap: 4px; padding: 10px 12px; border-radius: var(--pf-radius); border: 1px solid var(--pf-border); background: var(--pf-surface-2); }
|
|
636
706
|
.pf-ladder__tile--on { background: var(--pf-grant-soft); border-color: var(--pf-grant-border); color: var(--pf-grant); }
|
|
@@ -640,6 +710,7 @@
|
|
|
640
710
|
.pf-audit-footer { font-size: 12px; color: var(--pf-text-3); padding-top: 12px; border-top: 1px solid var(--pf-border); }
|
|
641
711
|
/* A link inside a line of text is told apart by more than its colour. */
|
|
642
712
|
.pf-audit-footer a, .pf-hint a { text-decoration: underline; }
|
|
713
|
+
.pf-audit-footer__hint { display: block; margin-top: 4px; }
|
|
643
714
|
.pf-tech > summary { font-size: 12px; color: var(--pf-text-3); cursor: pointer; width: max-content; }
|
|
644
715
|
.pf-tech__body { display: flex; flex-direction: column; gap: 8px; padding-top: 10px; font-size: 12px; color: var(--pf-text-2); }
|
|
645
716
|
.pf-copy { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
@@ -651,6 +722,7 @@
|
|
|
651
722
|
.pf-savebar__count { font-weight: 600; }
|
|
652
723
|
.pf-savebar__note { font-size: 12px; opacity: .8; }
|
|
653
724
|
.pf-savebar[data-pf-dirty="false"] .pf-savebar__count--dirty, .pf-savebar[data-pf-dirty="true"] .pf-savebar__count--clean { display: none; }
|
|
725
|
+
.pf-js .pf-savebar--quiet[data-pf-dirty="false"] { display: none; }
|
|
654
726
|
|
|
655
727
|
/* --------------------------------------------------------------- menus */
|
|
656
728
|
.pf-menu { position: relative; display: inline-block; }
|
|
@@ -688,9 +760,15 @@
|
|
|
688
760
|
.pf-close:hover { background: var(--pf-surface-2); }
|
|
689
761
|
.pf-confirm { display: flex; flex-direction: column; gap: 14px; padding: 22px; }
|
|
690
762
|
.pf-confirm__title { font-size: 17px; font-weight: 600; text-wrap: balance; }
|
|
691
|
-
.pf-confirm__list { max-height: 200px; overflow-y: auto; padding: 10px 12px; border-radius: var(--pf-radius); background: var(--pf-surface-2); font-size: 13px; display: flex; flex-direction: column; gap: 4px; }
|
|
763
|
+
.pf-confirm__list { position: relative; max-height: 200px; overflow-y: auto; padding: 10px 12px; border-radius: var(--pf-radius); background: var(--pf-surface-2); font-size: 13px; display: flex; flex-direction: column; gap: 4px; }
|
|
692
764
|
.pf-confirm--danger { border-top: 3px solid var(--pf-block); }
|
|
693
765
|
.pf-toasts { position: fixed; left: 50%; bottom: 24px; z-index: 60; transform: translateX(-50%); display: flex; flex-direction: column; gap: 8px; width: max-content; max-width: min(640px, calc(100% - 32px)); }
|
|
766
|
+
/* A page with a sticky savebar keeps the bottom edge for it: toasts go to the top, so neither hides the other. */
|
|
767
|
+
.pf-root:has(.pf-savebar) .pf-toasts { top: 16px; bottom: auto; }
|
|
768
|
+
/* Beside the sidebar (desktop widths) toasts centre over the main column, never over the sidebar's footer (language, theme). */
|
|
769
|
+
@media (min-width: 1024px) {
|
|
770
|
+
.pf-root:has(.pf-sidebar) .pf-toasts { left: calc(50% + var(--pf-sidebar-width) / 2); max-width: min(640px, calc(100% - var(--pf-sidebar-width) - 32px)); }
|
|
771
|
+
}
|
|
694
772
|
.pf-toast { display: flex; align-items: center; gap: 12px; padding: 10px 12px 10px 16px; border-radius: var(--pf-radius); background: var(--pf-text); color: var(--pf-surface); box-shadow: var(--pf-shadow); font-size: 13px; }
|
|
695
773
|
.pf-toast--alert { background: var(--pf-block); color: #fff; }
|
|
696
774
|
.pf-toast__text { flex: 1; min-width: 0; }
|
|
@@ -712,6 +790,8 @@
|
|
|
712
790
|
.pf-sync > summary { list-style: none; cursor: pointer; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; padding: 12px 16px; color: var(--pf-warn); font-weight: 600; font-size: 13px; }
|
|
713
791
|
.pf-sync > summary::-webkit-details-marker { display: none; }
|
|
714
792
|
.pf-sync__body { padding: 0 16px 16px; }
|
|
793
|
+
.pf-sync--info { border-color: var(--pf-border); background: var(--pf-surface); }
|
|
794
|
+
.pf-sync--info > summary { color: var(--pf-text-2); }
|
|
715
795
|
.pf-crud-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
|
|
716
796
|
.pf-crud-grid__col { display: flex; flex-direction: column; gap: 6px; }
|
|
717
797
|
.pf-scope-option { display: flex; gap: 8px; align-items: flex-start; }
|
|
@@ -762,5 +842,7 @@
|
|
|
762
842
|
.pf-reason__body { display: flex; flex-direction: column; gap: 4px; margin-top: 8px; max-width: 420px; }
|
|
763
843
|
.pf-reason--compact .pf-reason__summary { font-size: 12px; }
|
|
764
844
|
.pf-reason--compact .pf-reason__body { margin-top: 6px; min-width: 200px; }
|
|
765
|
-
|
|
845
|
+
/* min-width: 0 — as a flex item (a card header, .pf-actions) the form may shrink to the line, so a
|
|
846
|
+
people <select> with long labels shrinks with it instead of pushing the form past the page. */
|
|
847
|
+
.pf-reason-form { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; min-width: 0; }
|
|
766
848
|
.pf-reason-form .pf-reason--compact[open] { flex-basis: 100%; }
|