diff --git a/.github/workflows/version-sync.yml b/.github/workflows/version-sync.yml index 258ab7e6..bd496080 100644 --- a/.github/workflows/version-sync.yml +++ b/.github/workflows/version-sync.yml @@ -3,13 +3,15 @@ name: Sync version artifacts # Triggered whenever a GitHub release or pre-release is published. # Checks out main, aligns all non-JS version references to the release tag, # rebuilds CSS bundles, publishes them to the dist branch, captures the -# resulting commit SHA, stores it in SLASHED_BRICKS_DIST_SHA, and commits +# resulting commit SHA, stores it in SLASHED_DIST_SHA (slashed.php), +# SLASHED_BRICKS_DIST_SHA (slashed-bricks.php), and +# SLASHED_GUTENBERG_DIST_SHA (slashed-gutenberg.php), then commits # everything back to main. # # CSS bundles are NOT committed to the main branch — they live only on the # dedicated `dist` branch and in GitHub Release assets. The dist-branch -# commit SHA is stored in slashed-bricks.php so the plugin can construct an -# immutable jsDelivr CDN URL that never changes between releases. +# commit SHA is stored in each integration entry point so the plugin can +# construct an immutable jsDelivr CDN URL that never changes between releases. # # Full release pipeline: # 1. Developer runs `npm run release` locally → bumps package.json, runs @@ -106,12 +108,19 @@ jobs: - name: Switch back to main working tree run: git checkout main - - name: Update SLASHED_BRICKS_DIST_SHA in PHP + - name: Update DIST_SHA constants in PHP + env: + DIST_SHA: ${{ steps.dist.outputs.sha }} run: | - DIST_SHA="${{ steps.dist.outputs.sha }}" + sed -i \ + "s|define( 'SLASHED_DIST_SHA', '[^']*' )|define( 'SLASHED_DIST_SHA', '${DIST_SHA}' )|" \ + slashed.php sed -i \ "s|define( 'SLASHED_BRICKS_DIST_SHA', '[^']*' )|define( 'SLASHED_BRICKS_DIST_SHA', '${DIST_SHA}' )|" \ integrations/bricks/slashed-bricks.php + sed -i \ + "s|define( 'SLASHED_GUTENBERG_DIST_SHA', '[^']*' )|define( 'SLASHED_GUTENBERG_DIST_SHA', '${DIST_SHA}' )|" \ + integrations/gutenberg/slashed-gutenberg.php - name: Commit and push if anything changed env: @@ -120,7 +129,7 @@ jobs: git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" # Stage only tracked files (dist/*.css are gitignored). - git add integrations/bricks/slashed-bricks.php package.json package-lock.json + git add slashed.php integrations/bricks/slashed-bricks.php integrations/gutenberg/slashed-gutenberg.php package.json package-lock.json git add -u if git diff --cached --quiet; then echo "Nothing to commit — all version artifacts already up to date." diff --git a/.gitignore b/.gitignore index 3f324d91..c3721f8c 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,6 @@ semantic-review/ # Build artifacts in dist/ — generated by `npm run build`, served via the # `dist` branch (version-sync.yml) and GitHub Releases. Not tracked in main # to avoid binary conflicts and noise in PR diffs. -dist/slashed-bricks.zip +dist/slashed.zip dist/*.css dist/*.map diff --git a/docs/roadmap.md b/docs/roadmap.md index aa648ce1..0873edf1 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -110,12 +110,20 @@ stable and locked. Ideas that are promising but not yet committed. -- **Gutenberg integration** — CSS loading + color palette sync + `theme.json` - custom property mapping. Scoped v1 is ~2–3 weeks of work and covers 80% of - what Gutenberg users need. Deferred because the `--sf-*` → `--wp--custom--*` - naming mismatch needs a clean translation layer, and Gutenberg's per-block - architecture is fundamentally different from Bricks' canvas model. reBEMer - parity is not a goal for any Gutenberg integration. +- **Gutenberg integration** *(in progress — `integrations/gutenberg/`)* — v1 + ships as a standalone plugin (`slashed-gutenberg.php`) that can be activated + independently of the Bricks plugin. v1 scope: CSS bundle loading into the + block editor canvas + frontend, color palette sync (21 tokens — brand, + status, surface, text, border, link), and a `data-wp-dark-mode-active` bridge + so the editor dark-mode toggle drives SLASHED's color-scheme system. + + Deliberately excluded from v1 (future work): + - `--sf-*` → `--wp--custom--*` theme.json mapping — only needed to expose + tokens in the Global Styles UI (Site Editor); not required for the 80% use + case. Analogy: ACSS does not do this either. + - Token override admin UI — no Svelte panel for Gutenberg yet; overrides via + the `slashed_gutenberg/css_bundle_url` filter or child-theme CSS. + - reBEMer parity — Bricks-specific; not applicable to the block editor. - **Per-layer opt-in bundle via `@import`** — technically already possible today using the granular `core/` and `optional/` source files (with diff --git a/includes/class-admin.php b/includes/class-admin.php new file mode 100644 index 00000000..ef89c8f9 --- /dev/null +++ b/includes/class-admin.php @@ -0,0 +1,183 @@ + in_array( $raw_bundle, Slashed_Settings::ALLOWED_BUNDLES, true ) ? $raw_bundle : 'optimal', + 'integrations' => array(), + ); + foreach ( Slashed_Settings::KNOWN_INTEGRATIONS as $slug ) { + $data['integrations'][ $slug ] = array_key_exists( $slug, $raw_integrations ); + } + + Slashed_Settings::save( $data ); + + wp_safe_redirect( + add_query_arg( 'slashed_saved', '1', admin_url( 'admin.php?page=' . self::PAGE_SLUG ) ) + ); + exit; + } + + public function render_page() { + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + + $settings = Slashed_Settings::get(); + $integrations = $settings['integrations']; + $css_bundle = $settings['css_bundle']; + $saved = ! empty( $_GET['slashed_saved'] ); // phpcs:ignore WordPress.Security.NonceVerification + ?> +
+

+ + +

+ + +
+ + + +

+

+ + + + + + + + + + + + +

+

+ + + __( 'Essential — reset + design tokens only', 'slashed' ), + 'optimal' => __( 'Optimal — + layout, typography, states, forms (recommended)', 'slashed' ), + 'full' => __( 'Full — + utilities layer', 'slashed' ), + ); + foreach ( $bundles as $value => $label ) : + ?> + + + + + + + + +
+ +
+

+ ' . esc_html( substr( SLASHED_DIST_SHA, 0, 8 ) ) . '' + ); + ?> +

+
+ , css_bundle: string} + */ + public static function get() { + $stored = get_option( self::OPTION_KEY, array() ); + if ( ! is_array( $stored ) ) { + $stored = array(); + } + + return array( + 'integrations' => self::get_integrations( $stored ), + 'css_bundle' => self::get_css_bundle( $stored ), + ); + } + + /** + * Get the configured CSS bundle variant. + * + * Reads from shared settings; defaults to 'optimal'. + * Called directly by Slashed_CSS_Loader so integrations do not need to + * implement bundle resolution themselves. + * + * @param array|null $stored Pre-fetched stored option (avoids double DB read). + * @return string + */ + public static function get_css_bundle( $stored = null ) { + if ( null === $stored ) { + $stored = get_option( self::OPTION_KEY, array() ); + if ( ! is_array( $stored ) ) { + $stored = array(); + } + } + $bundle = isset( $stored['css_bundle'] ) ? (string) $stored['css_bundle'] : 'optimal'; + return in_array( $bundle, self::ALLOWED_BUNDLES, true ) ? $bundle : 'optimal'; + } + + /** + * Whether a given integration is enabled. + * + * Defaults to true so a fresh install activates every integration; + * users explicitly disable what they do not need. + * + * @param string $integration Integration slug (e.g. 'bricks', 'gutenberg'). + * @return bool + */ + public static function is_enabled( $integration ) { + $settings = self::get(); + $flags = $settings['integrations']; + // Unknown integrations default to false (future-compat: a new integration + // added in a later release should not silently activate on upgrade). + if ( ! in_array( $integration, self::KNOWN_INTEGRATIONS, true ) ) { + return false; + } + return isset( $flags[ $integration ] ) ? (bool) $flags[ $integration ] : true; + } + + /** + * Persist new settings values. + * + * Only known, sanitized keys are written. Unrecognised keys are silently + * dropped to prevent arbitrary data from landing in the option. + * + * @param array $data Raw submitted data (e.g. from $_POST). + * @return bool True if the value was updated, false if unchanged or invalid. + */ + public static function save( array $data ) { + $integrations = array(); + foreach ( self::KNOWN_INTEGRATIONS as $slug ) { + $integrations[ $slug ] = ! empty( $data['integrations'][ $slug ] ); + } + + $bundle = isset( $data['css_bundle'] ) ? (string) $data['css_bundle'] : 'optimal'; + if ( ! in_array( $bundle, self::ALLOWED_BUNDLES, true ) ) { + $bundle = 'optimal'; + } + + return update_option( self::OPTION_KEY, array( + 'integrations' => $integrations, + 'css_bundle' => $bundle, + ) ); + } + + /** + * @param array $stored Raw stored option value. + * @return array + */ + private static function get_integrations( array $stored ) { + $flags = isset( $stored['integrations'] ) && is_array( $stored['integrations'] ) + ? $stored['integrations'] + : array(); + + $result = array(); + foreach ( self::KNOWN_INTEGRATIONS as $slug ) { + // Default true: new installs run all integrations until turned off. + $result[ $slug ] = isset( $flags[ $slug ] ) ? (bool) $flags[ $slug ] : true; + } + return $result; + } +} diff --git a/integrations/bricks/includes/class-admin-page-svelte.php b/integrations/bricks/includes/class-admin-page-svelte.php index 2c701e00..4274db20 100644 --- a/integrations/bricks/includes/class-admin-page-svelte.php +++ b/integrations/bricks/includes/class-admin-page-svelte.php @@ -54,18 +54,34 @@ public function __construct() { } /** - * Register the top-level SLASHED admin menu. + * Register the SLASHED admin menu. + * + * Standalone plugin: registers a top-level "SLASHED" menu. + * Unified plugin (SLASHED_VERSION defined): registers as a sub-page + * under the top-level "SLASHED" menu owned by slashed.php so only one + * SLASHED item appears in the sidebar. */ public function register_menu() { - $this->hook_suffix = (string) add_menu_page( - __( 'SLASHED Settings', 'slashed-bricks' ), - __( 'SLASHED', 'slashed-bricks' ), - 'manage_options', - self::PAGE_SLUG, - array( $this, 'render_page' ), - 'dashicons-art', - 59 - ); + if ( defined( 'SLASHED_VERSION' ) ) { + $this->hook_suffix = (string) add_submenu_page( + 'slashed', + __( 'Bricks Tokens', 'slashed-bricks' ), + __( 'Bricks Tokens', 'slashed-bricks' ), + 'manage_options', + self::PAGE_SLUG, + array( $this, 'render_page' ) + ); + } else { + $this->hook_suffix = (string) add_menu_page( + __( 'SLASHED Settings', 'slashed-bricks' ), + __( 'SLASHED', 'slashed-bricks' ), + 'manage_options', + self::PAGE_SLUG, + array( $this, 'render_page' ), + 'dashicons-art', + 59 + ); + } } /** diff --git a/integrations/bricks/includes/class-enqueue.php b/integrations/bricks/includes/class-enqueue.php index 649c81be..9ecc1a0c 100644 --- a/integrations/bricks/includes/class-enqueue.php +++ b/integrations/bricks/includes/class-enqueue.php @@ -61,20 +61,17 @@ public function enqueue_frontend_styles() { return; } - // Use file modification time for cache-busting when the CSS exists locally. - // Derive the filename from the already-resolved URL so we don't re-read - // plugin settings a second time (slashed_bricks_get_css_url() already did). - $filename = basename( (string) wp_parse_url( $css_url, PHP_URL_PATH ) ); - - if ( '' === $filename ) { - $version = SLASHED_BRICKS_VERSION; + // Cache-busting: mtime for local files, version constant for CDN. + if ( class_exists( 'Slashed_CSS_Loader' ) ) { + $version = Slashed_CSS_Loader::get_version( $css_url ); } else { + $filename = basename( (string) wp_parse_url( $css_url, PHP_URL_PATH ) ); $repo_path = SLASHED_BRICKS_PATH . '../../dist/' . $filename; $local_path = SLASHED_BRICKS_PATH . 'dist/' . $filename; - if ( file_exists( $repo_path ) ) { + if ( '' !== $filename && file_exists( $repo_path ) ) { $version = (string) filemtime( $repo_path ); - } elseif ( file_exists( $local_path ) ) { + } elseif ( '' !== $filename && file_exists( $local_path ) ) { $version = (string) filemtime( $local_path ); } else { $version = SLASHED_BRICKS_VERSION; diff --git a/integrations/bricks/slashed-bricks.php b/integrations/bricks/slashed-bricks.php index 0df90311..3b906811 100644 --- a/integrations/bricks/slashed-bricks.php +++ b/integrations/bricks/slashed-bricks.php @@ -18,44 +18,39 @@ /** * Plugin constants. - */ -define( 'SLASHED_BRICKS_VERSION', '0.4.18' ); -define( 'SLASHED_BRICKS_PATH', plugin_dir_path( __FILE__ ) ); -define( 'SLASHED_BRICKS_URL', plugin_dir_url( __FILE__ ) ); - -/** - * Immutable jsDelivr ref for the SLASHED CSS bundle. * - * Points to the HEAD commit SHA of the `dist` branch at the time of the - * last release. jsDelivr treats commit SHAs as effectively immutable - * (cached "forever"), so the served CSS cannot change outside a plugin - * release even though the `dist` branch itself is a moving ref. - * - * CSS bundles are no longer committed to the main branch; they live only - * on the `dist` branch (files at root) and in GitHub Release assets. - * Updated automatically by the version-sync workflow on every release. - * - * Override per-site with the 'slashed_bricks/css_bundle_url' filter. + * Defined via plugin_dir_path(__FILE__) so paths are correct whether this + * file is loaded standalone or included by the unified slashed.php. The + * !defined() guard exists solely to prevent redefinition errors when both + * the standalone Bricks plugin and the unified SLASHED plugin are active. */ -define( 'SLASHED_BRICKS_CSS_REF', 'v0.4.18' ); // semver tag — version comparison only -define( 'SLASHED_BRICKS_DIST_SHA', 'be9ac0789180158c8ad86d5743020ef2272a063c' ); // dist branch SHA — CDN URL +if ( ! defined( 'SLASHED_BRICKS_VERSION' ) ) { + define( 'SLASHED_BRICKS_VERSION', '0.4.18' ); + define( 'SLASHED_BRICKS_PATH', plugin_dir_path( __FILE__ ) ); + define( 'SLASHED_BRICKS_URL', plugin_dir_url( __FILE__ ) ); + define( 'SLASHED_BRICKS_CSS_REF', 'v0.4.18' ); + define( 'SLASHED_BRICKS_DIST_SHA', 'be9ac0789180158c8ad86d5743020ef2272a063c' ); +} /** * Token Store is loaded early so slashed_bricks_get_css_bundle() can call * Slashed_Bricks_Token_Store::get_plugin_settings() at any point, even * before the rest of the admin/data classes are initialised. */ -require_once plugin_dir_path( __FILE__ ) . 'includes/class-token-store.php'; +require_once SLASHED_BRICKS_PATH . 'includes/class-token-store.php'; /** - * Get the configured CSS bundle type (essential / optimal / full). + * Get the configured CSS bundle variant. * - * Reads from plugin settings; falls back to "optimal". Used both for - * URL resolution and for local-file version-stamp lookups. + * Delegates to the shared Slashed_CSS_Loader when running under the unified + * plugin; falls back to the Bricks token store in standalone mode. * * @return string One of 'essential', 'optimal', 'full'. */ function slashed_bricks_get_css_bundle() { + if ( class_exists( 'Slashed_CSS_Loader' ) ) { + return Slashed_CSS_Loader::get_bundle(); + } $settings = Slashed_Bricks_Token_Store::get_plugin_settings(); $bundle = isset( $settings['css_bundle'] ) ? (string) $settings['css_bundle'] : 'optimal'; if ( ! in_array( $bundle, Slashed_Bricks_Token_Store::ALLOWED_CSS_BUNDLES, true ) ) { @@ -67,44 +62,35 @@ function slashed_bricks_get_css_bundle() { /** * Get the URL for the SLASHED CSS bundle. * - * Defaults to the jsDelivr CDN pinned to the immutable dist-branch commit - * SHA (see SLASHED_BRICKS_DIST_SHA) so the plugin works without any local - * file setup. The specific file (essential / optimal / full) is chosen - * from the 'css_bundle' plugin setting. If a local copy is detected - * (symlink/in-repo mode or copy-install mode), the local file takes - * precedence for faster loads and offline development. + * Delegates to the shared Slashed_CSS_Loader when running under the unified + * plugin, then applies the per-integration filter. In standalone mode, builds + * the URL directly from SLASHED_BRICKS_DIST_SHA with a local-file fallback. * * Use the 'slashed_bricks/css_bundle_url' filter to override. * * @return string URL to the CSS bundle. */ function slashed_bricks_get_css_url() { - $bundle = slashed_bricks_get_css_bundle(); - $filename = 'slashed.' . $bundle . '.css'; + if ( class_exists( 'Slashed_CSS_Loader' ) ) { + return apply_filters( 'slashed_bricks/css_bundle_url', Slashed_CSS_Loader::get_url() ); + } - // Default: jsDelivr CDN pinned to the dist-branch commit SHA at release - // time. Files live at the root of the dist branch (no /dist/ prefix). + // Standalone fallback. + $bundle = slashed_bricks_get_css_bundle(); + $filename = 'slashed.' . $bundle . '.css'; $default_url = sprintf( 'https://cdn.jsdelivr.net/gh/codeslash-dev/SLASHED@%s/%s', SLASHED_BRICKS_DIST_SHA, $filename ); - // Prefer local file if available (symlink/in-repo mode). $repo_path = SLASHED_BRICKS_PATH . '../../dist/' . $filename; if ( file_exists( $repo_path ) ) { $default_url = SLASHED_BRICKS_URL . '../../dist/' . $filename; - } - // Check copy-install mode (dist/ within the plugin directory). - elseif ( file_exists( SLASHED_BRICKS_PATH . 'dist/' . $filename ) ) { + } elseif ( file_exists( SLASHED_BRICKS_PATH . 'dist/' . $filename ) ) { $default_url = SLASHED_BRICKS_URL . 'dist/' . $filename; } - /** - * Filter the SLASHED CSS bundle URL. - * - * @param string $url The URL to the CSS bundle file. - */ return apply_filters( 'slashed_bricks/css_bundle_url', $default_url ); } @@ -270,7 +256,11 @@ function slashed_bricks_activation_check() { // Allow activation without Bricks so admin token configuration is available. // Runtime guards in slashed_bricks_init() handle the Bricks dependency. } -register_activation_hook( __FILE__, 'slashed_bricks_activation_check' ); +// Only register hooks against the standalone plugin file; when running under +// the unified slashed.php the root plugin handles activation/deactivation. +if ( ! defined( 'SLASHED_VERSION' ) ) { + register_activation_hook( __FILE__, 'slashed_bricks_activation_check' ); +} /** * Display admin notice when Bricks Builder is not active. @@ -376,7 +366,9 @@ function slashed_bricks_deactivation_cleanup() { wp_unschedule_event( $timestamp, 'slashed_bricks_version_check' ); } } -register_deactivation_hook( __FILE__, 'slashed_bricks_deactivation_cleanup' ); +if ( ! defined( 'SLASHED_VERSION' ) ) { + register_deactivation_hook( __FILE__, 'slashed_bricks_deactivation_cleanup' ); +} /** * Dashboard widget content: informs the user a newer framework version exists. diff --git a/integrations/gutenberg/includes/class-color-palette.php b/integrations/gutenberg/includes/class-color-palette.php new file mode 100644 index 00000000..1390bc82 --- /dev/null +++ b/integrations/gutenberg/includes/class-color-palette.php @@ -0,0 +1,134 @@ +build_palette() ); + // Block/FSE themes: theme.json takes precedence; inject via the data filter. + add_filter( 'wp_theme_json_data_theme', array( $this, 'inject_theme_json_palette' ) ); + } + + /** + * Inject the SLASHED palette into the theme.json data for block/FSE themes. + * + * Uses WP_Theme_JSON_Data::update_with() which merges presets by slug, so + * the theme's own palette entries are preserved alongside ours. All SLASHED + * slugs are prefixed with 'slashed-' to avoid collisions. + * + * @param WP_Theme_JSON_Data $theme_json Theme JSON data object. + * @return WP_Theme_JSON_Data + */ + public function inject_theme_json_palette( $theme_json ) { + $theme_json->update_with( array( + 'version' => 3, + 'settings' => array( + 'color' => array( + 'palette' => $this->build_palette(), + ), + ), + ) ); + return $theme_json; + } + + /** + * Build the full palette array from the token definitions. + * + * @return array[] + */ + private function build_palette() { + $entries = array(); + + foreach ( $this->get_definitions() as $def ) { + $entries[] = array( + 'name' => $def['name'], + 'slug' => 'slashed-' . $def['slug'], + 'color' => 'var(--sf-color-' . $def['token'] . ')', + ); + } + + return $entries; + } + + /** + * Token definitions: slug (WP class-name fragment), label, CSS token suffix. + * + * `slug` — appended to 'slashed-' for the WP color slug (single dashes only). + * `token` — the `--sf-color-{token}` custom property suffix (may contain --). + * `name` — human-readable label shown in the color picker. + * + * @return array[] + */ + private function get_definitions() { + return array( + // Brand + array( 'slug' => 'primary', 'token' => 'primary', 'name' => __( 'Primary', 'slashed-gutenberg' ) ), + array( 'slug' => 'secondary', 'token' => 'secondary', 'name' => __( 'Secondary', 'slashed-gutenberg' ) ), + array( 'slug' => 'tertiary', 'token' => 'tertiary', 'name' => __( 'Tertiary', 'slashed-gutenberg' ) ), + array( 'slug' => 'action', 'token' => 'action', 'name' => __( 'Action', 'slashed-gutenberg' ) ), + array( 'slug' => 'neutral', 'token' => 'neutral', 'name' => __( 'Neutral', 'slashed-gutenberg' ) ), + array( 'slug' => 'base', 'token' => 'base', 'name' => __( 'Base', 'slashed-gutenberg' ) ), + // Status + array( 'slug' => 'success', 'token' => 'success', 'name' => __( 'Success', 'slashed-gutenberg' ) ), + array( 'slug' => 'warning', 'token' => 'warning', 'name' => __( 'Warning', 'slashed-gutenberg' ) ), + array( 'slug' => 'error', 'token' => 'error', 'name' => __( 'Error', 'slashed-gutenberg' ) ), + array( 'slug' => 'info', 'token' => 'info', 'name' => __( 'Info', 'slashed-gutenberg' ) ), + array( 'slug' => 'danger', 'token' => 'danger', 'name' => __( 'Danger', 'slashed-gutenberg' ) ), + // Surface + array( 'slug' => 'bg', 'token' => 'bg', 'name' => __( 'Background', 'slashed-gutenberg' ) ), + array( 'slug' => 'surface', 'token' => 'surface', 'name' => __( 'Surface', 'slashed-gutenberg' ) ), + array( 'slug' => 'well', 'token' => 'well', 'name' => __( 'Well', 'slashed-gutenberg' ) ), + array( 'slug' => 'raised', 'token' => 'raised', 'name' => __( 'Raised', 'slashed-gutenberg' ) ), + array( 'slug' => 'inverse', 'token' => 'inverse', 'name' => __( 'Inverse', 'slashed-gutenberg' ) ), + // Text + array( 'slug' => 'text', 'token' => 'text', 'name' => __( 'Text', 'slashed-gutenberg' ) ), + array( 'slug' => 'text-secondary', 'token' => 'text--secondary', 'name' => __( 'Text Secondary', 'slashed-gutenberg' ) ), + array( 'slug' => 'text-muted', 'token' => 'text--muted', 'name' => __( 'Text Muted', 'slashed-gutenberg' ) ), + // Border & link + array( 'slug' => 'border', 'token' => 'border', 'name' => __( 'Border', 'slashed-gutenberg' ) ), + array( 'slug' => 'link', 'token' => 'link', 'name' => __( 'Link', 'slashed-gutenberg' ) ), + ); + } +} diff --git a/integrations/gutenberg/includes/class-enqueue.php b/integrations/gutenberg/includes/class-enqueue.php new file mode 100644 index 00000000..c0003103 --- /dev/null +++ b/integrations/gutenberg/includes/class-enqueue.php @@ -0,0 +1,110 @@ + (WP 6.4+). An inline rule maps that attribute into SLASHED's + * theme layer so the editing surface tracks the editor's dark-mode toggle. + * The rule is harmless on the frontend where the attribute is never set. + */ +class Slashed_Gutenberg_Enqueue { + + public function __construct() { + add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_editor_styles' ) ); + add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_styles' ) ); + } + + /** + * Load SLASHED CSS into the block editor. + * + * `enqueue_block_editor_assets` fires for both the post editor and the + * site editor (FSE). Styles registered here are injected directly into + * the editor iframe so they affect the editing canvas, not the + * surrounding wp-admin chrome. + */ + public function enqueue_editor_styles() { + $css_url = slashed_gutenberg_get_css_url(); + if ( '' === $css_url ) { + return; + } + + wp_enqueue_style( + 'slashed-framework', + $css_url, + array(), + $this->get_version( $css_url ) + ); + + // Bridge the Gutenberg dark-mode toggle to SLASHED's theme system. + wp_add_inline_style( + 'slashed-framework', + '@layer slashed.themes{html[data-wp-dark-mode-active]{color-scheme:dark;--sf-is-dark:1}}' + ); + } + + /** + * Load SLASHED CSS on the public frontend. + */ + public function enqueue_frontend_styles() { + $css_url = slashed_gutenberg_get_css_url(); + if ( '' === $css_url ) { + return; + } + + wp_enqueue_style( + 'slashed-framework', + $css_url, + array(), + $this->get_version( $css_url ) + ); + } + + /** + * Derive a cache-busting version string from the local file mtime when + * available; falls back to the plugin version constant. + * + * @param string $css_url Resolved URL for the bundle. + * @return string + */ + private function get_version( $css_url ) { + if ( class_exists( 'Slashed_CSS_Loader' ) ) { + return Slashed_CSS_Loader::get_version( $css_url ); + } + + // Standalone fallback. + $filename = basename( (string) wp_parse_url( $css_url, PHP_URL_PATH ) ); + if ( '' === $filename ) { + return SLASHED_GUTENBERG_VERSION; + } + + $repo_path = SLASHED_GUTENBERG_PATH . '../../dist/' . $filename; + $local_path = SLASHED_GUTENBERG_PATH . 'dist/' . $filename; + + if ( file_exists( $repo_path ) ) { + return (string) filemtime( $repo_path ); + } + if ( file_exists( $local_path ) ) { + return (string) filemtime( $local_path ); + } + + return SLASHED_GUTENBERG_VERSION; + } +} diff --git a/integrations/gutenberg/slashed-gutenberg.php b/integrations/gutenberg/slashed-gutenberg.php new file mode 100644 index 00000000..32be0629 --- /dev/null +++ b/integrations/gutenberg/slashed-gutenberg.php @@ -0,0 +1,106 @@ +>> 1) ^ (crc & 1 ? 0xedb88320 : 0); - } + for (let j = 0; j < 8; j++) crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0); } return (crc ^ 0xffffffff) >>> 0; } diff --git a/slashed.php b/slashed.php new file mode 100644 index 00000000..88473e5b --- /dev/null +++ b/slashed.php @@ -0,0 +1,81 @@ +